diff --git a/src/Markdig.Benchmarks/CommonMarkLib.cs b/src/Markdig.Benchmarks/CommonMarkLib.cs index f5be92905..7751dfec1 100644 --- a/src/Markdig.Benchmarks/CommonMarkLib.cs +++ b/src/Markdig.Benchmarks/CommonMarkLib.cs @@ -1,40 +1,38 @@ -// Copyright (c) Alexandre Mutel. All rights reserved. +// 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.Runtime.InteropServices; using System.Text; -namespace Testamina.Markdig.Benchmarks +namespace Testamina.Markdig.Benchmarks; + +public static class CommonMarkLib { - public static class CommonMarkLib + public static string ToHtml(string text) { - public static string ToHtml(string text) + unsafe { - unsafe - { - var textAsArray = Encoding.UTF8.GetBytes(text); + var textAsArray = Encoding.UTF8.GetBytes(text); - fixed (void* ptext = textAsArray) + fixed (void* ptext = textAsArray) + { + var ptr = (byte*)cmark_markdown_to_html(new IntPtr(ptext), text.Length); + int length = 0; + while (ptr[length] != 0) { - var ptr = (byte*)cmark_markdown_to_html(new IntPtr(ptext), text.Length); - int length = 0; - while (ptr[length] != 0) - { - length++; - } - var buffer = new byte[length]; - Marshal.Copy(new IntPtr(ptr), buffer, 0, length); - var result = Encoding.UTF8.GetString(buffer); - Marshal.FreeHGlobal(new IntPtr(ptr)); - return result; + length++; } + var buffer = new byte[length]; + Marshal.Copy(new IntPtr(ptr), buffer, 0, length); + var result = Encoding.UTF8.GetString(buffer); + Marshal.FreeHGlobal(new IntPtr(ptr)); + return result; } } - - // char *cmark_markdown_to_html(const char *text, size_t len, int options); - [DllImport("cmark", CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr cmark_markdown_to_html(IntPtr charBuffer, int len, int options = 0); } + + // char *cmark_markdown_to_html(const char *text, size_t len, int options); + [DllImport("cmark", CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr cmark_markdown_to_html(IntPtr charBuffer, int len, int options = 0); } \ No newline at end of file diff --git a/src/Markdig.Benchmarks/Markdig.Benchmarks.csproj b/src/Markdig.Benchmarks/Markdig.Benchmarks.csproj index 5cbc77278..894470cf3 100644 --- a/src/Markdig.Benchmarks/Markdig.Benchmarks.csproj +++ b/src/Markdig.Benchmarks/Markdig.Benchmarks.csproj @@ -4,6 +4,7 @@ Exe true false + enable diff --git a/src/Markdig.Benchmarks/Program.cs b/src/Markdig.Benchmarks/Program.cs index 277b376f1..7433f43dd 100644 --- a/src/Markdig.Benchmarks/Program.cs +++ b/src/Markdig.Benchmarks/Program.cs @@ -2,76 +2,75 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using System.IO; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Running; + using Markdig; -namespace Testamina.Markdig.Benchmarks +namespace Testamina.Markdig.Benchmarks; + +//[BenchmarkTask(platform: BenchmarkPlatform.X64, jitVersion: BenchmarkJitVersion.RyuJit, processCount: 1, warmupIterationCount: 2)] +public class Program { - //[BenchmarkTask(platform: BenchmarkPlatform.X64, jitVersion: BenchmarkJitVersion.RyuJit, processCount: 1, warmupIterationCount: 2)] - public class Program - { - private string text; + private string text; - public Program() - { - //text = File.ReadAllText("progit.md"); - text = File.ReadAllText("spec.md"); - } + public Program() + { + //text = File.ReadAllText("progit.md"); + text = File.ReadAllText("spec.md"); + } - //[Benchmark(Description = "TestMarkdig", OperationsPerInvoke = 4096)] - [Benchmark(Description = "markdig")] - public void TestMarkdig() - { - //var reader = new StreamReader(File.Open("spec.md", FileMode.Open)); - Markdown.ToHtml(text); - //File.WriteAllText("spec.html", writer.ToString()); - } + //[Benchmark(Description = "TestMarkdig", OperationsPerInvoke = 4096)] + [Benchmark(Description = "markdig")] + public void TestMarkdig() + { + //var reader = new StreamReader(File.Open("spec.md", FileMode.Open)); + Markdown.ToHtml(text); + //File.WriteAllText("spec.html", writer.ToString()); + } - [Benchmark(Description = "cmark")] - public void TestCommonMarkCpp() - { - //var reader = new StreamReader(File.Open("spec.md", FileMode.Open)); - CommonMarkLib.ToHtml(text); - //File.WriteAllText("spec.html", writer.ToString()); - } + [Benchmark(Description = "cmark")] + public void TestCommonMarkCpp() + { + //var reader = new StreamReader(File.Open("spec.md", FileMode.Open)); + CommonMarkLib.ToHtml(text); + //File.WriteAllText("spec.html", writer.ToString()); + } - [Benchmark(Description = "CommonMark.NET")] - public void TestCommonMarkNet() - { - ////var reader = new StreamReader(File.Open("spec.md", FileMode.Open)); - // var reader = new StringReader(text); - //CommonMark.CommonMarkConverter.Parse(reader); - //CommonMark.CommonMarkConverter.Parse(reader); - //reader.Dispose(); - //var writer = new StringWriter(); - CommonMark.CommonMarkConverter.Convert(text); - //writer.Flush(); - //writer.ToString(); - } + [Benchmark(Description = "CommonMark.NET")] + public void TestCommonMarkNet() + { + ////var reader = new StreamReader(File.Open("spec.md", FileMode.Open)); + // var reader = new StringReader(text); + //CommonMark.CommonMarkConverter.Parse(reader); + //CommonMark.CommonMarkConverter.Parse(reader); + //reader.Dispose(); + //var writer = new StringWriter(); + CommonMark.CommonMarkConverter.Convert(text); + //writer.Flush(); + //writer.ToString(); + } - [Benchmark(Description = "MarkdownSharp")] - public void TestMarkdownSharp() - { - new MarkdownSharp.Markdown().Transform(text); - } + [Benchmark(Description = "MarkdownSharp")] + public void TestMarkdownSharp() + { + new MarkdownSharp.Markdown().Transform(text); + } - static void Main(string[] args) - { - var config = ManualConfig.Create(DefaultConfig.Instance); - //var gcDiagnoser = new MemoryDiagnoser(); - //config.Add(new Job { Mode = Mode.SingleRun, LaunchCount = 2, WarmupCount = 2, IterationTime = 1024, TargetCount = 10 }); - //config.Add(new Job { Mode = Mode.Throughput, LaunchCount = 2, WarmupCount = 2, TargetCount = 10 }); - //config.Add(gcDiagnoser); + static void Main(string[] args) + { + var config = ManualConfig.Create(DefaultConfig.Instance); + //var gcDiagnoser = new MemoryDiagnoser(); + //config.Add(new Job { Mode = Mode.SingleRun, LaunchCount = 2, WarmupCount = 2, IterationTime = 1024, TargetCount = 10 }); + //config.Add(new Job { Mode = Mode.Throughput, LaunchCount = 2, WarmupCount = 2, TargetCount = 10 }); + //config.Add(gcDiagnoser); - //var config = DefaultConfig.Instance; - BenchmarkRunner.Run(config); - //BenchmarkRunner.Run(config); - //BenchmarkRunner.Run(); - //BenchmarkRunner.Run(); - } + //var config = DefaultConfig.Instance; + BenchmarkRunner.Run(config); + //BenchmarkRunner.Run(config); + //BenchmarkRunner.Run(); + //BenchmarkRunner.Run(); } } diff --git a/src/Markdig.Benchmarks/TestMatchPerf.cs b/src/Markdig.Benchmarks/TestMatchPerf.cs index 030b96f9e..25ede8957 100644 --- a/src/Markdig.Benchmarks/TestMatchPerf.cs +++ b/src/Markdig.Benchmarks/TestMatchPerf.cs @@ -2,81 +2,79 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; -namespace Testamina.Markdig.Benchmarks +namespace Testamina.Markdig.Benchmarks; + +public class TextRegexHelper { - public class TextRegexHelper + private readonly Dictionary replacers; + private readonly Regex regex; + + public TextRegexHelper(Dictionary replacers) { - private readonly Dictionary replacers; - private readonly Regex regex; + this.replacers = replacers; + var builder = new StringBuilder(); - public TextRegexHelper(Dictionary replacers) + // (?<1>:value:?)|(?<1>:noo:?) + foreach (var replace in replacers) { - this.replacers = replacers; - var builder = new StringBuilder(); - - // (?<1>:value:?)|(?<1>:noo:?) - foreach (var replace in replacers) + var matchStr = Regex.Escape(replace.Key); + if (builder.Length > 0) { - var matchStr = Regex.Escape(replace.Key); - if (builder.Length > 0) - { - builder.Append('|'); - } - builder.Append("(?<1>").Append(matchStr).Append("?)"); + builder.Append('|'); } - - regex = new Regex(builder.ToString()); + builder.Append("(?<1>").Append(matchStr).Append("?)"); } - public bool TryMatch(string text, int offset, out string matchText, out string replaceText) - { - replaceText = null; - matchText = null; - var result = regex.Match(text, offset); - if (!result.Success) - { - return false; - } + regex = new Regex(builder.ToString()); + } - matchText = result.Groups[1].Value; - replaceText = replacers[matchText]; - return true; + public bool TryMatch(string text, int offset, out string matchText, out string replaceText) + { + replaceText = null; + matchText = null; + var result = regex.Match(text, offset); + if (!result.Success) + { + return false; } + + matchText = result.Groups[1].Value; + replaceText = replacers[matchText]; + return true; } +} - /* - public class TestMatchPerf - { - private readonly TextMatchHelper matcher; +/* +public class TestMatchPerf +{ + private readonly TextMatchHelper matcher; - public TestMatchPerf() + public TestMatchPerf() + { + var replacers = new Dictionary(); + for (int i = 0; i < 1000; i++) { - var replacers = new Dictionary(); - for (int i = 0; i < 1000; i++) - { - replacers.Add($":z{i}:", i.ToString()); - } - replacers.Add(":abc:", "yes"); - - matcher = new TextMatchHelper(new HashSet(replacers.Keys)); + replacers.Add($":z{i}:", i.ToString()); } + replacers.Add(":abc:", "yes"); - [Benchmark] - public void TestMatch() - { + matcher = new TextMatchHelper(new HashSet(replacers.Keys)); + } - for (int i = 0; i < 1000; i++) - { - string matchText; - //var text = ":z150: this is a long string"; - var text = ":z1:"; - matcher.TryMatch(text, 0, text.Length, out matchText); - } + [Benchmark] + public void TestMatch() + { + + for (int i = 0; i < 1000; i++) + { + string matchText; + //var text = ":z150: this is a long string"; + var text = ":z1:"; + matcher.TryMatch(text, 0, text.Length, out matchText); } } - */ -} \ No newline at end of file +} +*/ \ No newline at end of file diff --git a/src/Markdig.Benchmarks/TestStringPerf.cs b/src/Markdig.Benchmarks/TestStringPerf.cs index 32d0fea30..ae5a3ace1 100644 --- a/src/Markdig.Benchmarks/TestStringPerf.cs +++ b/src/Markdig.Benchmarks/TestStringPerf.cs @@ -1,58 +1,57 @@ -using System.IO; using BenchmarkDotNet.Attributes; + using Markdig.Renderers; -namespace Testamina.Markdig.Benchmarks +namespace Testamina.Markdig.Benchmarks; + +public class TestStringPerf { - public class TestStringPerf - { - private string text; + private string text; - public TestStringPerf() - { - text = new string('a', 1000); - } + public TestStringPerf() + { + text = new string('a', 1000); + } - [Benchmark] - public void TestIndexOfAny() + [Benchmark] + public void TestIndexOfAny() + { + var writer = new HtmlRenderer(new StringWriter()); + for (int i = 0; i < 100; i++) { - var writer = new HtmlRenderer(new StringWriter()); - for (int i = 0; i < 100; i++) - { - writer.WriteEscape(text, 0, text.Length); - writer.WriteEscape(text, 0, text.Length); - writer.WriteEscape(text, 0, text.Length); - writer.WriteEscape(text, 0, text.Length); - writer.WriteEscape(text, 0, text.Length); - - writer.WriteEscape(text, 0, text.Length); - writer.WriteEscape(text, 0, text.Length); - writer.WriteEscape(text, 0, text.Length); - writer.WriteEscape(text, 0, text.Length); - writer.WriteEscape(text, 0, text.Length); - } + writer.WriteEscape(text, 0, text.Length); + writer.WriteEscape(text, 0, text.Length); + writer.WriteEscape(text, 0, text.Length); + writer.WriteEscape(text, 0, text.Length); + writer.WriteEscape(text, 0, text.Length); + + writer.WriteEscape(text, 0, text.Length); + writer.WriteEscape(text, 0, text.Length); + writer.WriteEscape(text, 0, text.Length); + writer.WriteEscape(text, 0, text.Length); + writer.WriteEscape(text, 0, text.Length); } + } - //[Benchmark] - //public void TestCustomIndexOfAny() - //{ - // var writer = new HtmlRenderer(new StringWriter()); - // for (int i = 0; i < 100; i++) - // { - // writer.WriteEscapeOptimized(text, 0, text.Length); - // writer.WriteEscapeOptimized(text, 0, text.Length); - // writer.WriteEscapeOptimized(text, 0, text.Length); - // writer.WriteEscapeOptimized(text, 0, text.Length); - // writer.WriteEscapeOptimized(text, 0, text.Length); - - // writer.WriteEscapeOptimized(text, 0, text.Length); - // writer.WriteEscapeOptimized(text, 0, text.Length); - // writer.WriteEscapeOptimized(text, 0, text.Length); - // writer.WriteEscapeOptimized(text, 0, text.Length); - // writer.WriteEscapeOptimized(text, 0, text.Length); - // } - //} + //[Benchmark] + //public void TestCustomIndexOfAny() + //{ + // var writer = new HtmlRenderer(new StringWriter()); + // for (int i = 0; i < 100; i++) + // { + // writer.WriteEscapeOptimized(text, 0, text.Length); + // writer.WriteEscapeOptimized(text, 0, text.Length); + // writer.WriteEscapeOptimized(text, 0, text.Length); + // writer.WriteEscapeOptimized(text, 0, text.Length); + // writer.WriteEscapeOptimized(text, 0, text.Length); + + // writer.WriteEscapeOptimized(text, 0, text.Length); + // writer.WriteEscapeOptimized(text, 0, text.Length); + // writer.WriteEscapeOptimized(text, 0, text.Length); + // writer.WriteEscapeOptimized(text, 0, text.Length); + // writer.WriteEscapeOptimized(text, 0, text.Length); + // } + //} - } } \ No newline at end of file diff --git a/src/Markdig.Tests/Markdig.Tests.csproj b/src/Markdig.Tests/Markdig.Tests.csproj index 09c5ffca2..88887715b 100644 --- a/src/Markdig.Tests/Markdig.Tests.csproj +++ b/src/Markdig.Tests/Markdig.Tests.csproj @@ -4,6 +4,7 @@ net6.0 Exe false + enable Markdig.Tests.Program $(MSBuildProjectDirectory)\..\SpecFileGen\bin\$(Configuration)\net6.0\SpecFileGen.dll $(MSBuildProjectDirectory)\..\SpecFileGen\bin\$(Configuration)\net6.0\SpecFileGen.timestamp @@ -20,6 +21,10 @@ + + + + diff --git a/src/Markdig.Tests/MiscTests.cs b/src/Markdig.Tests/MiscTests.cs index bdbfde288..f3793ebfd 100644 --- a/src/Markdig.Tests/MiscTests.cs +++ b/src/Markdig.Tests/MiscTests.cs @@ -1,192 +1,191 @@ -using System; -using System.IO; -using System.Linq; using System.Text.RegularExpressions; + using Markdig.Extensions.AutoLinks; + using NUnit.Framework; -namespace Markdig.Tests +namespace Markdig.Tests; + +public class MiscTests { - public class MiscTests + [Test] + public void LinkWithInvalidNonAsciiDomainNameIsIgnored() { - [Test] - public void LinkWithInvalidNonAsciiDomainNameIsIgnored() - { - // Url from https://github.com/lunet-io/markdig/issues/438 - _ = Markdown.ToHtml("[minulém díle](http://V%20minulém%20díle%20jsme%20nainstalovali%20SQL%20Server,%20který%20je%20nutný%20pro%20běh%20Configuration%20Manageru.%20Dnes%20nás%20čeká%20instalace%20WSUS,%20což%20je%20produkt,%20jež%20je%20možné%20používat%20i%20jako%20samostatnou%20funkci%20ve%20Windows%20Serveru,%20který%20se%20stará%20o%20stažení%20a%20instalaci%20aktualizací%20z%20Microsoft%20Update%20na%20klientské%20počítače.%20Stejně%20jako%20v%20předchozích%20dílech,%20tak%20i%20v%20tomto%20si%20ukážeme%20obě%20varianty%20instalace%20–%20a%20to%20jak%20instalaci%20z%20PowerShellu,%20tak%20instalaci%20pomocí%20GUI.) "); + // Url from https://github.com/lunet-io/markdig/issues/438 + _ = Markdown.ToHtml("[minulém díle](http://V%20minulém%20díle%20jsme%20nainstalovali%20SQL%20Server,%20který%20je%20nutný%20pro%20běh%20Configuration%20Manageru.%20Dnes%20nás%20čeká%20instalace%20WSUS,%20což%20je%20produkt,%20jež%20je%20možné%20používat%20i%20jako%20samostatnou%20funkci%20ve%20Windows%20Serveru,%20který%20se%20stará%20o%20stažení%20a%20instalaci%20aktualizací%20z%20Microsoft%20Update%20na%20klientské%20počítače.%20Stejně%20jako%20v%20předchozích%20dílech,%20tak%20i%20v%20tomto%20si%20ukážeme%20obě%20varianty%20instalace%20–%20a%20to%20jak%20instalaci%20z%20PowerShellu,%20tak%20instalaci%20pomocí%20GUI.) "); - // Valid IDN - TestParser.TestSpec("[foo](http://ünicode.com)", "

foo

"); - TestParser.TestSpec("[foo](http://ünicode.ünicode.com)", "

foo

"); + // Valid IDN + TestParser.TestSpec("[foo](http://ünicode.com)", "

foo

"); + TestParser.TestSpec("[foo](http://ünicode.ünicode.com)", "

foo

"); - // Invalid IDN - TestParser.TestSpec("[foo](http://ünicode..com)", "

foo

"); - } + // Invalid IDN + TestParser.TestSpec("[foo](http://ünicode..com)", "

foo

"); + } - [TestCase("link [foo [bar]]")] // https://spec.commonmark.org/0.29/#example-508 - [TestCase("link [foo][bar]")] - [TestCase("link [][foo][bar][]")] - [TestCase("link [][foo][bar][[]]")] - [TestCase("link [foo] [bar]")] - [TestCase("link [[foo] [] [bar] [[abc]def]]")] - [TestCase("[]")] - [TestCase("[ ]")] - [TestCase("[bar][]")] - [TestCase("[bar][ foo]")] - [TestCase("[bar][foo ][]")] - [TestCase("[bar][fo[ ]o ][][]")] - [TestCase("[a]b[c[d[e]f]g]h")] - [TestCase("a[b[c[d]e]f[g]h]i foo [j]k[l[m]n]o")] - [TestCase("a[b[c[d]e]f[g]h]i[] [][foo][bar][] foo [j]k[l[m]n]o")] - [TestCase("a[b[c[d]e]f[g]h]i foo [j]k[l[m]n]o[][]")] - public void LinkTextMayContainBalancedBrackets(string linkText) - { - string markdown = $"[{linkText}](/uri)"; - string expected = $@"

{linkText}

"; + [TestCase("link [foo [bar]]")] // https://spec.commonmark.org/0.29/#example-508 + [TestCase("link [foo][bar]")] + [TestCase("link [][foo][bar][]")] + [TestCase("link [][foo][bar][[]]")] + [TestCase("link [foo] [bar]")] + [TestCase("link [[foo] [] [bar] [[abc]def]]")] + [TestCase("[]")] + [TestCase("[ ]")] + [TestCase("[bar][]")] + [TestCase("[bar][ foo]")] + [TestCase("[bar][foo ][]")] + [TestCase("[bar][fo[ ]o ][][]")] + [TestCase("[a]b[c[d[e]f]g]h")] + [TestCase("a[b[c[d]e]f[g]h]i foo [j]k[l[m]n]o")] + [TestCase("a[b[c[d]e]f[g]h]i[] [][foo][bar][] foo [j]k[l[m]n]o")] + [TestCase("a[b[c[d]e]f[g]h]i foo [j]k[l[m]n]o[][]")] + public void LinkTextMayContainBalancedBrackets(string linkText) + { + string markdown = $"[{linkText}](/uri)"; + string expected = $@"

{linkText}

"; - TestParser.TestSpec(markdown, expected); + TestParser.TestSpec(markdown, expected); - // Make the link text unbalanced - foreach (var bracketIndex in linkText - .Select((c, i) => new Tuple(c, i)) - .Where(t => t.Item1 == '[' || t.Item1 == ']') - .Select(t => t.Item2)) - { - string brokenLinkText = linkText.Remove(bracketIndex, 1); + // Make the link text unbalanced + foreach (var bracketIndex in linkText + .Select((c, i) => new Tuple(c, i)) + .Where(t => t.Item1 == '[' || t.Item1 == ']') + .Select(t => t.Item2)) + { + string brokenLinkText = linkText.Remove(bracketIndex, 1); - markdown = $"[{brokenLinkText}](/uri)"; - expected = $@"

{brokenLinkText}

"; + markdown = $"[{brokenLinkText}](/uri)"; + expected = $@"

{brokenLinkText}

"; - string actual = Markdown.ToHtml(markdown); - Assert.AreNotEqual(expected, actual); - } + string actual = Markdown.ToHtml(markdown); + Assert.AreNotEqual(expected, actual); } + } - [Theory] - [TestCase('[', 9 * 1024, true, false)] - [TestCase('[', 11 * 1024, true, true)] - [TestCase('[', 100, false, false)] - [TestCase('[', 150, false, true)] - [TestCase('>', 100, true, false)] - [TestCase('>', 150, true, true)] - public void GuardsAgainstHighlyNestedNodes(char c, int count, bool parseOnly, bool shouldThrow) + [Theory] + [TestCase('[', 9 * 1024, true, false)] + [TestCase('[', 11 * 1024, true, true)] + [TestCase('[', 100, false, false)] + [TestCase('[', 150, false, true)] + [TestCase('>', 100, true, false)] + [TestCase('>', 150, true, true)] + public void GuardsAgainstHighlyNestedNodes(char c, int count, bool parseOnly, bool shouldThrow) + { + var markdown = new string(c, count); + TestDelegate test = parseOnly ? () => Markdown.Parse(markdown) : () => Markdown.ToHtml(markdown); + + if (shouldThrow) { - var markdown = new string(c, count); - TestDelegate test = parseOnly ? () => Markdown.Parse(markdown) : () => Markdown.ToHtml(markdown); - - if (shouldThrow) - { - Exception e = Assert.Throws(test); - Assert.True(e.Message.Contains("depth limit")); - } - else - { - test(); - } + Exception e = Assert.Throws(test); + Assert.True(e.Message.Contains("depth limit")); } - - [Test] - public void IsIssue356Corrected() + else { - string input = @"https://foo.bar/path/\#m4mv5W0GYKZpGvfA.97"; - string expected = @"

https://foo.bar/path/\#m4mv5W0GYKZpGvfA.97

"; - - TestParser.TestSpec($"<{input}>", expected); - TestParser.TestSpec(input, expected, "autolinks|advanced"); + test(); } + } - [Test] - public void IsIssue365Corrected() - { - // The scheme must be escaped too... - string input = "![image](\"onclick=\"alert&#40;'click'&#41;\"://)"; - string expected = "

\"image\"

"; + [Test] + public void IsIssue356Corrected() + { + string input = @"https://foo.bar/path/\#m4mv5W0GYKZpGvfA.97"; + string expected = @"

https://foo.bar/path/\#m4mv5W0GYKZpGvfA.97

"; - TestParser.TestSpec(input, expected); - } + TestParser.TestSpec($"<{input}>", expected); + TestParser.TestSpec(input, expected, "autolinks|advanced"); + } - [Test] - public void TestAltTextIsCorrectlyEscaped() - { - TestParser.TestSpec( - @"![This is image alt text with quotation ' and double quotation ""hello"" world](girl.png)", - @"

"); - } + [Test] + public void IsIssue365Corrected() + { + // The scheme must be escaped too... + string input = "![image](\"onclick=\"alert&#40;'click'&#41;\"://)"; + string expected = "

\"image\"

"; - [Test] - public void TestChangelogPRLinksMatchDescription() - { - string solutionFolder = Path.GetFullPath(Path.Combine(TestParser.TestsDirectory, "../..")); - string changelogPath = Path.Combine(solutionFolder, "changelog.md"); - string changelog = File.ReadAllText(changelogPath); - var matches = Regex.Matches(changelog, @"\(\[\(PR #(\d+)\)\]\(.*?pull\/(\d+)\)\)"); - Assert.Greater(matches.Count, 0); - foreach (Match match in matches) - { - Assert.True(int.TryParse(match.Groups[1].Value, out int textNr)); - Assert.True(int.TryParse(match.Groups[2].Value, out int linkNr)); - Assert.AreEqual(textNr, linkNr); - } - } + TestParser.TestSpec(input, expected); + } - [Test] - public void TestFixHang() - { - var input = File.ReadAllText(Path.Combine(TestParser.TestsDirectory, "hang.md")); - _ = Markdown.ToHtml(input); - } + [Test] + public void TestAltTextIsCorrectlyEscaped() + { + TestParser.TestSpec( + @"![This is image alt text with quotation ' and double quotation ""hello"" world](girl.png)", + @"

"); + } - [Test] - public void TestInvalidHtmlEntity() + [Test] + public void TestChangelogPRLinksMatchDescription() + { + string solutionFolder = Path.GetFullPath(Path.Combine(TestParser.TestsDirectory, "../..")); + string changelogPath = Path.Combine(solutionFolder, "changelog.md"); + string changelog = File.ReadAllText(changelogPath); + var matches = Regex.Matches(changelog, @"\(\[\(PR #(\d+)\)\]\(.*?pull\/(\d+)\)\)"); + Assert.Greater(matches.Count, 0); + foreach (Match match in matches) { - var input = "9&ddr;&*&ddr;&de��__"; - TestParser.TestSpec(input, "

9&ddr;&*&ddr;&de��__

"); + Assert.True(int.TryParse(match.Groups[1].Value, out int textNr)); + Assert.True(int.TryParse(match.Groups[2].Value, out int linkNr)); + Assert.AreEqual(textNr, linkNr); } + } - [Test] - public void TestInvalidCharacterHandling() - { - var input = File.ReadAllText(Path.Combine(TestParser.TestsDirectory, "ArgumentOutOfRangeException.md")); - _ = Markdown.ToHtml(input); - } + [Test] + public void TestFixHang() + { + var input = File.ReadAllText(Path.Combine(TestParser.TestsDirectory, "hang.md")); + _ = Markdown.ToHtml(input); + } - [Test] - public void TestInvalidCodeEscape() - { - var input = "```**Header** "; - _ = Markdown.ToHtml(input); - } + [Test] + public void TestInvalidHtmlEntity() + { + var input = "9&ddr;&*&ddr;&de��__"; + TestParser.TestSpec(input, "

9&ddr;&*&ddr;&de��__

"); + } - [Test] - public void TestEmphasisAndHtmlEntity() - { - var markdownText = "*Unlimited-Fun®*®"; - TestParser.TestSpec(markdownText, "

Unlimited-Fun®®

"); - } + [Test] + public void TestInvalidCharacterHandling() + { + var input = File.ReadAllText(Path.Combine(TestParser.TestsDirectory, "ArgumentOutOfRangeException.md")); + _ = Markdown.ToHtml(input); + } - [Test] - public void TestThematicInsideCodeBlockInsideList() - { - var input = @"1. In the : + [Test] + public void TestInvalidCodeEscape() + { + var input = "```**Header** "; + _ = Markdown.ToHtml(input); + } + + [Test] + public void TestEmphasisAndHtmlEntity() + { + var markdownText = "*Unlimited-Fun®*®"; + TestParser.TestSpec(markdownText, "

Unlimited-Fun®®

"); + } + + [Test] + public void TestThematicInsideCodeBlockInsideList() + { + var input = @"1. In the : ``` Id DisplayName Description -- ----------- ----------- 62375ab9-6b52-47ed-826b-58e47e0e304b Group.Unified ... ```"; - TestParser.TestSpec(input, @"
    + TestParser.TestSpec(input, @"
    1. In the :

      Id                                   DisplayName         Description
       --                                   -----------         -----------
       62375ab9-6b52-47ed-826b-58e47e0e304b Group.Unified       ...
       
    "); - } + } - [Test] - public void VisualizeMathExpressions() - { - string math = @"Math expressions + [Test] + public void VisualizeMathExpressions() + { + string math = @"Math expressions $\frac{n!}{k!(n-k)!} = \binom{n}{k}$ @@ -206,91 +205,90 @@ public void VisualizeMathExpressions() \end{align} "; - //Console.WriteLine("Math Expressions:\n"); - var pl = new MarkdownPipelineBuilder().UseMathematics().Build(); // UseEmphasisExtras(EmphasisExtraOptions.Subscript).Build() - var html = Markdown.ToHtml(math, pl); - //Console.WriteLine(html); - } + //Console.WriteLine("Math Expressions:\n"); + var pl = new MarkdownPipelineBuilder().UseMathematics().Build(); // UseEmphasisExtras(EmphasisExtraOptions.Subscript).Build() + var html = Markdown.ToHtml(math, pl); + //Console.WriteLine(html); + } - [Test] - public void InlineMathExpression() - { - string math = @"Math expressions + [Test] + public void InlineMathExpression() + { + string math = @"Math expressions $\frac{n!}{k!(n-k)!} = \binom{n}{k}$ "; - var pl = new MarkdownPipelineBuilder().UseMathematics().Build(); // UseEmphasisExtras(EmphasisExtraOptions.Subscript).Build() - - var html = Markdown.ToHtml(math, pl); + var pl = new MarkdownPipelineBuilder().UseMathematics().Build(); // UseEmphasisExtras(EmphasisExtraOptions.Subscript).Build() - var test1 = html.Contains("

    \\("); - var test2 = html.Contains("\\)

    "); - if (!test1 || !test2) - { - Console.WriteLine(html); - } + var html = Markdown.ToHtml(math, pl); - Assert.IsTrue(test1, "Leading bracket missing"); - Assert.IsTrue(test2, "Trailing bracket missing"); + var test1 = html.Contains("

    \\("); + var test2 = html.Contains("\\)

    "); + if (!test1 || !test2) + { + Console.WriteLine(html); } - [Test] - public void BlockMathExpression() - { - string math = @"Math expressions + Assert.IsTrue(test1, "Leading bracket missing"); + Assert.IsTrue(test2, "Trailing bracket missing"); + } + + [Test] + public void BlockMathExpression() + { + string math = @"Math expressions $$ \frac{n!}{k!(n-k)!} = \binom{n}{k} $$ "; - var pl = new MarkdownPipelineBuilder().UseMathematics().Build(); // UseEmphasisExtras(EmphasisExtraOptions.Subscript).Build() - - var html = Markdown.ToHtml(math, pl); - var test1 = html.Contains("
    \n\\["); - var test2 = html.Contains("\\]
    "); - if (!test1 || !test2) - { - Console.WriteLine(html); - } - - Assert.IsTrue(test1, "Leading bracket missing"); - Assert.IsTrue(test2, "Trailing bracket missing"); - } + var pl = new MarkdownPipelineBuilder().UseMathematics().Build(); // UseEmphasisExtras(EmphasisExtraOptions.Subscript).Build() - [Test] - public void CanDisableParsingHeadings() + var html = Markdown.ToHtml(math, pl); + var test1 = html.Contains("
    \n\\["); + var test2 = html.Contains("\\]
    "); + if (!test1 || !test2) { - var noHeadingsPipeline = new MarkdownPipelineBuilder().DisableHeadings().Build(); + Console.WriteLine(html); + } - TestParser.TestSpec("Foo\n===", "

    Foo

    "); - TestParser.TestSpec("Foo\n===", "

    Foo\n===

    ", noHeadingsPipeline); + Assert.IsTrue(test1, "Leading bracket missing"); + Assert.IsTrue(test2, "Trailing bracket missing"); + } - TestParser.TestSpec("# Heading 1", "

    Heading 1

    "); - TestParser.TestSpec("# Heading 1", "

    # Heading 1

    ", noHeadingsPipeline); + [Test] + public void CanDisableParsingHeadings() + { + var noHeadingsPipeline = new MarkdownPipelineBuilder().DisableHeadings().Build(); - // Does not also disable link reference definitions - TestParser.TestSpec("[Foo]\n\n[Foo]: bar", "

    Foo

    "); - TestParser.TestSpec("[Foo]\n\n[Foo]: bar", "

    Foo

    ", noHeadingsPipeline); - } + TestParser.TestSpec("Foo\n===", "

    Foo

    "); + TestParser.TestSpec("Foo\n===", "

    Foo\n===

    ", noHeadingsPipeline); - [Test] - public void CanOpenAutoLinksInNewWindow() - { - var pipeline = new MarkdownPipelineBuilder().UseAutoLinks().Build(); - var newWindowPipeline = new MarkdownPipelineBuilder().UseAutoLinks(new AutoLinkOptions() { OpenInNewWindow = true }).Build(); + TestParser.TestSpec("# Heading 1", "

    Heading 1

    "); + TestParser.TestSpec("# Heading 1", "

    # Heading 1

    ", noHeadingsPipeline); - TestParser.TestSpec("www.foo.bar", "

    www.foo.bar

    ", pipeline); - TestParser.TestSpec("www.foo.bar", "

    www.foo.bar

    ", newWindowPipeline); - } + // Does not also disable link reference definitions + TestParser.TestSpec("[Foo]\n\n[Foo]: bar", "

    Foo

    "); + TestParser.TestSpec("[Foo]\n\n[Foo]: bar", "

    Foo

    ", noHeadingsPipeline); + } - [Test] - public void CanUseHttpsPrefixForWWWAutoLinks() - { - var pipeline = new MarkdownPipelineBuilder().UseAutoLinks().Build(); - var httpsPipeline = new MarkdownPipelineBuilder().UseAutoLinks(new AutoLinkOptions() { UseHttpsForWWWLinks = true }).Build(); + [Test] + public void CanOpenAutoLinksInNewWindow() + { + var pipeline = new MarkdownPipelineBuilder().UseAutoLinks().Build(); + var newWindowPipeline = new MarkdownPipelineBuilder().UseAutoLinks(new AutoLinkOptions() { OpenInNewWindow = true }).Build(); - TestParser.TestSpec("www.foo.bar", "

    www.foo.bar

    ", pipeline); - TestParser.TestSpec("www.foo.bar", "

    www.foo.bar

    ", httpsPipeline); - } + TestParser.TestSpec("www.foo.bar", "

    www.foo.bar

    ", pipeline); + TestParser.TestSpec("www.foo.bar", "

    www.foo.bar

    ", newWindowPipeline); + } + + [Test] + public void CanUseHttpsPrefixForWWWAutoLinks() + { + var pipeline = new MarkdownPipelineBuilder().UseAutoLinks().Build(); + var httpsPipeline = new MarkdownPipelineBuilder().UseAutoLinks(new AutoLinkOptions() { UseHttpsForWWWLinks = true }).Build(); + + TestParser.TestSpec("www.foo.bar", "

    www.foo.bar

    ", pipeline); + TestParser.TestSpec("www.foo.bar", "

    www.foo.bar

    ", httpsPipeline); } } diff --git a/src/Markdig.Tests/Program.cs b/src/Markdig.Tests/Program.cs index 564e9a9ea..dc47be724 100644 --- a/src/Markdig.Tests/Program.cs +++ b/src/Markdig.Tests/Program.cs @@ -2,18 +2,15 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using System; +namespace Markdig.Tests; -namespace Markdig.Tests +class Program { - class Program + public static void Main(string[] args) { - public static void Main(string[] args) - { - Console.WriteLine("Run NUnit tests runner with this"); - // Uncomment the following line to debug a specific tests more easily - //var tests = new Specs.CommonMarkV_0_29.TestLeafBlocksSetextHeadings(); - //tests.LeafBlocksSetextHeadings_Example063(); - } + Console.WriteLine("Run NUnit tests runner with this"); + // Uncomment the following line to debug a specific tests more easily + //var tests = new Specs.CommonMarkV_0_29.TestLeafBlocksSetextHeadings(); + //tests.LeafBlocksSetextHeadings_Example063(); } } \ No newline at end of file diff --git a/src/Markdig.Tests/RoundtripSpecs/TestAtxHeading.cs b/src/Markdig.Tests/RoundtripSpecs/TestAtxHeading.cs index 4b6607952..51110f4f3 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestAtxHeading.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestAtxHeading.cs @@ -1,55 +1,53 @@ -using NUnit.Framework; using static Markdig.Tests.TestRoundtrip; -namespace Markdig.Tests.RoundtripSpecs +namespace Markdig.Tests.RoundtripSpecs; + +[TestFixture] +public class TestAtxHeading { - [TestFixture] - public class TestAtxHeading + [TestCase("# h")] + [TestCase("# h ")] + [TestCase("# h\n#h")] + [TestCase("# h\n #h")] + [TestCase("# h\n # h")] + [TestCase("# h\n # h ")] + [TestCase(" # h \n # h ")] + public void Test(string value) { - [TestCase("# h")] - [TestCase("# h ")] - [TestCase("# h\n#h")] - [TestCase("# h\n #h")] - [TestCase("# h\n # h")] - [TestCase("# h\n # h ")] - [TestCase(" # h \n # h ")] - public void Test(string value) - { - RoundTrip(value); - } + RoundTrip(value); + } - [TestCase("\n# h\n\np")] - [TestCase("\n# h\n\np\n")] - [TestCase("\n# h\n\np\n\n")] - [TestCase("\n\n# h\n\np\n\n")] - [TestCase("\n\n# h\np\n\n")] - [TestCase("\n\n# h\np\n\n")] - public void TestParagraph(string value) - { - RoundTrip(value); - } + [TestCase("\n# h\n\np")] + [TestCase("\n# h\n\np\n")] + [TestCase("\n# h\n\np\n\n")] + [TestCase("\n\n# h\n\np\n\n")] + [TestCase("\n\n# h\np\n\n")] + [TestCase("\n\n# h\np\n\n")] + public void TestParagraph(string value) + { + RoundTrip(value); + } - [TestCase("\n# h")] - [TestCase("\n# h\n")] - [TestCase("\n# h\r")] - [TestCase("\n# h\r\n")] + [TestCase("\n# h")] + [TestCase("\n# h\n")] + [TestCase("\n# h\r")] + [TestCase("\n# h\r\n")] - [TestCase("\r# h")] - [TestCase("\r# h\n")] - [TestCase("\r# h\r")] - [TestCase("\r# h\r\n")] + [TestCase("\r# h")] + [TestCase("\r# h\n")] + [TestCase("\r# h\r")] + [TestCase("\r# h\r\n")] - [TestCase("\r\n# h")] - [TestCase("\r\n# h\n")] - [TestCase("\r\n# h\r")] - [TestCase("\r\n# h\r\n")] + [TestCase("\r\n# h")] + [TestCase("\r\n# h\n")] + [TestCase("\r\n# h\r")] + [TestCase("\r\n# h\r\n")] - [TestCase("# h\n\n ")] - [TestCase("# h\n\n ")] - [TestCase("# h\n\n ")] - public void TestNewline(string value) - { - RoundTrip(value); - } + [TestCase("# h\n\n ")] + [TestCase("# h\n\n ")] + [TestCase("# h\n\n ")] + public void TestNewline(string value) + { + RoundTrip(value); } } diff --git a/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs index b49fcc633..5361928af 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs @@ -1,107 +1,105 @@ -using NUnit.Framework; using static Markdig.Tests.TestRoundtrip; -namespace Markdig.Tests.RoundtripSpecs +namespace Markdig.Tests.RoundtripSpecs; + +[TestFixture] +public class TestFencedCodeBlock { - [TestFixture] - public class TestFencedCodeBlock - { - [TestCase("```\nc\n```")] - [TestCase("```\nc\n```\n")] - [TestCase("\n```\nc\n```")] - [TestCase("\n\n```\nc\n```")] - [TestCase("```\nc\n```\n")] - [TestCase("```\nc\n```\n\n")] - [TestCase("\n```\nc\n```\n")] - [TestCase("\n```\nc\n```\n\n")] - [TestCase("\n\n```\nc\n```\n")] - [TestCase("\n\n```\nc\n```\n\n")] + [TestCase("```\nc\n```")] + [TestCase("```\nc\n```\n")] + [TestCase("\n```\nc\n```")] + [TestCase("\n\n```\nc\n```")] + [TestCase("```\nc\n```\n")] + [TestCase("```\nc\n```\n\n")] + [TestCase("\n```\nc\n```\n")] + [TestCase("\n```\nc\n```\n\n")] + [TestCase("\n\n```\nc\n```\n")] + [TestCase("\n\n```\nc\n```\n\n")] - [TestCase(" ```\nc\n````")] - [TestCase("```\nc\n````")] - [TestCase("p\n\n```\nc\n```")] + [TestCase(" ```\nc\n````")] + [TestCase("```\nc\n````")] + [TestCase("p\n\n```\nc\n```")] - [TestCase("```\n c\n```")] - [TestCase("```\nc \n```")] - [TestCase("```\n c \n```")] + [TestCase("```\n c\n```")] + [TestCase("```\nc \n```")] + [TestCase("```\n c \n```")] - [TestCase(" ``` \n c \n ``` ")] - [TestCase("\t```\t\n\tc\t\n\t```\t")] - [TestCase("\v```\v\n\vc\v\n\v```\v")] - [TestCase("\f```\f\n\fc\f\n\f```\f")] - public void Test(string value) - { - RoundTrip(value); - } + [TestCase(" ``` \n c \n ``` ")] + [TestCase("\t```\t\n\tc\t\n\t```\t")] + [TestCase("\v```\v\n\vc\v\n\v```\v")] + [TestCase("\f```\f\n\fc\f\n\f```\f")] + public void Test(string value) + { + RoundTrip(value); + } - [TestCase("~~~ aa ``` ~~~\nfoo\n~~~")] - [TestCase("~~~ aa ``` ~~~\nfoo\n~~~ ")] - public void TestTilde(string value) - { - RoundTrip(value); - } + [TestCase("~~~ aa ``` ~~~\nfoo\n~~~")] + [TestCase("~~~ aa ``` ~~~\nfoo\n~~~ ")] + public void TestTilde(string value) + { + RoundTrip(value); + } - [TestCase("```\n c \n```")] - [TestCase("```\n c \r```")] - [TestCase("```\n c \r\n```")] - [TestCase("```\r c \n```")] - [TestCase("```\r c \r```")] - [TestCase("```\r c \r\n```")] - [TestCase("```\r\n c \n```")] - [TestCase("```\r\n c \r```")] - [TestCase("```\r\n c \r\n```")] + [TestCase("```\n c \n```")] + [TestCase("```\n c \r```")] + [TestCase("```\n c \r\n```")] + [TestCase("```\r c \n```")] + [TestCase("```\r c \r```")] + [TestCase("```\r c \r\n```")] + [TestCase("```\r\n c \n```")] + [TestCase("```\r\n c \r```")] + [TestCase("```\r\n c \r\n```")] - [TestCase("```\n c \n```\n")] - [TestCase("```\n c \r```\n")] - [TestCase("```\n c \r\n```\n")] - [TestCase("```\r c \n```\n")] - [TestCase("```\r c \r```\n")] - [TestCase("```\r c \r\n```\n")] - [TestCase("```\r\n c \n```\n")] - [TestCase("```\r\n c \r```\n")] - [TestCase("```\r\n c \r\n```\n")] + [TestCase("```\n c \n```\n")] + [TestCase("```\n c \r```\n")] + [TestCase("```\n c \r\n```\n")] + [TestCase("```\r c \n```\n")] + [TestCase("```\r c \r```\n")] + [TestCase("```\r c \r\n```\n")] + [TestCase("```\r\n c \n```\n")] + [TestCase("```\r\n c \r```\n")] + [TestCase("```\r\n c \r\n```\n")] - [TestCase("```\n c \n```\r")] - [TestCase("```\n c \r```\r")] - [TestCase("```\n c \r\n```\r")] - [TestCase("```\r c \n```\r")] - [TestCase("```\r c \r```\r")] - [TestCase("```\r c \r\n```\r")] - [TestCase("```\r\n c \n```\r")] - [TestCase("```\r\n c \r```\r")] - [TestCase("```\r\n c \r\n```\r")] + [TestCase("```\n c \n```\r")] + [TestCase("```\n c \r```\r")] + [TestCase("```\n c \r\n```\r")] + [TestCase("```\r c \n```\r")] + [TestCase("```\r c \r```\r")] + [TestCase("```\r c \r\n```\r")] + [TestCase("```\r\n c \n```\r")] + [TestCase("```\r\n c \r```\r")] + [TestCase("```\r\n c \r\n```\r")] - [TestCase("```\n c \n```\r\n")] - [TestCase("```\n c \r```\r\n")] - [TestCase("```\n c \r\n```\r\n")] - [TestCase("```\r c \n```\r\n")] - [TestCase("```\r c \r```\r\n")] - [TestCase("```\r c \r\n```\r\n")] - [TestCase("```\r\n c \n```\r\n")] - [TestCase("```\r\n c \r```\r\n")] - [TestCase("```\r\n c \r\n```\r\n")] - public void TestNewline(string value) - { - RoundTrip(value); - } + [TestCase("```\n c \n```\r\n")] + [TestCase("```\n c \r```\r\n")] + [TestCase("```\n c \r\n```\r\n")] + [TestCase("```\r c \n```\r\n")] + [TestCase("```\r c \r```\r\n")] + [TestCase("```\r c \r\n```\r\n")] + [TestCase("```\r\n c \n```\r\n")] + [TestCase("```\r\n c \r```\r\n")] + [TestCase("```\r\n c \r\n```\r\n")] + public void TestNewline(string value) + { + RoundTrip(value); + } - [TestCase("```i a\n```")] - [TestCase("```i a a2\n```")] - [TestCase("```i a a2 a3\n```")] - [TestCase("```i a a2 a3 a4\n```")] + [TestCase("```i a\n```")] + [TestCase("```i a a2\n```")] + [TestCase("```i a a2 a3\n```")] + [TestCase("```i a a2 a3 a4\n```")] - [TestCase("```i\ta\n```")] - [TestCase("```i\ta a2\n```")] - [TestCase("```i\ta a2 a3\n```")] - [TestCase("```i\ta a2 a3 a4\n```")] + [TestCase("```i\ta\n```")] + [TestCase("```i\ta a2\n```")] + [TestCase("```i\ta a2 a3\n```")] + [TestCase("```i\ta a2 a3 a4\n```")] - [TestCase("```i\ta \n```")] - [TestCase("```i\ta a2 \n```")] - [TestCase("```i\ta a2 a3 \n```")] - [TestCase("```i\ta a2 a3 a4 \n```")] - public void TestInfoArguments(string value) - { - RoundTrip(value); - } + [TestCase("```i\ta \n```")] + [TestCase("```i\ta a2 \n```")] + [TestCase("```i\ta a2 a3 \n```")] + [TestCase("```i\ta a2 a3 a4 \n```")] + public void TestInfoArguments(string value) + { + RoundTrip(value); } } diff --git a/src/Markdig.Tests/RoundtripSpecs/TestHtmlBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestHtmlBlock.cs index d40217cf7..9f2860b85 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestHtmlBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestHtmlBlock.cs @@ -1,20 +1,18 @@ -using NUnit.Framework; using static Markdig.Tests.TestRoundtrip; -namespace Markdig.Tests.RoundtripSpecs +namespace Markdig.Tests.RoundtripSpecs; + +[TestFixture] +public class TestHtmlBlock { - [TestFixture] - public class TestHtmlBlock + [TestCase("
    ")] + [TestCase("
    \n")] + [TestCase("
    \n\n")] + [TestCase("
    \n\n# h")] + [TestCase("p\n\n
    \n")] + [TestCase("
    \n\n# h")] + public void Test(string value) { - [TestCase("
    ")] - [TestCase("
    \n")] - [TestCase("
    \n\n")] - [TestCase("
    \n\n# h")] - [TestCase("p\n\n
    \n")] - [TestCase("
    \n\n# h")] - public void Test(string value) - { - RoundTrip(value); - } + RoundTrip(value); } } diff --git a/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs index 0a4935797..724a01f64 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs @@ -1,86 +1,84 @@ -using NUnit.Framework; using static Markdig.Tests.TestRoundtrip; -namespace Markdig.Tests.RoundtripSpecs +namespace Markdig.Tests.RoundtripSpecs; + +[TestFixture] +public class TestIndentedCodeBlock { - [TestFixture] - public class TestIndentedCodeBlock - { - // A codeblock is indented with 4 spaces. After the 4th space, whitespace is interpreted as content. - // l = line - [TestCase(" l")] - [TestCase(" l")] - [TestCase("\tl")] - [TestCase("\t\tl")] - [TestCase("\tl1\n l1")] + // A codeblock is indented with 4 spaces. After the 4th space, whitespace is interpreted as content. + // l = line + [TestCase(" l")] + [TestCase(" l")] + [TestCase("\tl")] + [TestCase("\t\tl")] + [TestCase("\tl1\n l1")] - [TestCase("\n l")] - [TestCase("\n\n l")] - [TestCase("\n l\n")] - [TestCase("\n l\n\n")] - [TestCase("\n\n l\n")] - [TestCase("\n\n l\n\n")] + [TestCase("\n l")] + [TestCase("\n\n l")] + [TestCase("\n l\n")] + [TestCase("\n l\n\n")] + [TestCase("\n\n l\n")] + [TestCase("\n\n l\n\n")] - [TestCase(" l\n l")] - [TestCase(" l\n l\n l")] + [TestCase(" l\n l")] + [TestCase(" l\n l\n l")] - // two newlines are needed for indented codeblock start after paragraph - [TestCase("p\n\n l")] - [TestCase("p\n\n l\n")] - [TestCase("p\n\n l\n\n")] + // two newlines are needed for indented codeblock start after paragraph + [TestCase("p\n\n l")] + [TestCase("p\n\n l\n")] + [TestCase("p\n\n l\n\n")] - [TestCase("p\n\n l\n l")] - [TestCase("p\n\n l\n l")] + [TestCase("p\n\n l\n l")] + [TestCase("p\n\n l\n l")] - [TestCase(" l\n\np\n\n l")] - [TestCase(" l l\n\np\n\n l l")] - public void Test(string value) - { - RoundTrip(value); - } + [TestCase(" l\n\np\n\n l")] + [TestCase(" l l\n\np\n\n l l")] + public void Test(string value) + { + RoundTrip(value); + } - [TestCase(" l\n")] - [TestCase(" l\r")] - [TestCase(" l\r\n")] + [TestCase(" l\n")] + [TestCase(" l\r")] + [TestCase(" l\r\n")] - [TestCase(" l\n l")] - [TestCase(" l\n l\n")] - [TestCase(" l\n l\r")] - [TestCase(" l\n l\r\n")] + [TestCase(" l\n l")] + [TestCase(" l\n l\n")] + [TestCase(" l\n l\r")] + [TestCase(" l\n l\r\n")] - [TestCase(" l\r l")] - [TestCase(" l\r l\n")] - [TestCase(" l\r l\r")] - [TestCase(" l\r l\r\n")] + [TestCase(" l\r l")] + [TestCase(" l\r l\n")] + [TestCase(" l\r l\r")] + [TestCase(" l\r l\r\n")] - [TestCase(" l\r\n l")] - [TestCase(" l\r\n l\n")] - [TestCase(" l\r\n l\r")] - [TestCase(" l\r\n l\r\n")] - public void TestNewline(string value) - { - RoundTrip(value); - } + [TestCase(" l\r\n l")] + [TestCase(" l\r\n l\n")] + [TestCase(" l\r\n l\r")] + [TestCase(" l\r\n l\r\n")] + public void TestNewline(string value) + { + RoundTrip(value); + } - [TestCase(" l\n\n l\n")] - [TestCase(" l\n\n\n l\n")] - public void TestNewlinesInBetweenResultInOneCodeBlock(string value) - { - var pipelineBuilder = new MarkdownPipelineBuilder(); - pipelineBuilder.EnableTrackTrivia(); - MarkdownPipeline pipeline = pipelineBuilder.Build(); - var markdownDocument = Markdown.Parse(value, pipeline); + [TestCase(" l\n\n l\n")] + [TestCase(" l\n\n\n l\n")] + public void TestNewlinesInBetweenResultInOneCodeBlock(string value) + { + var pipelineBuilder = new MarkdownPipelineBuilder(); + pipelineBuilder.EnableTrackTrivia(); + MarkdownPipeline pipeline = pipelineBuilder.Build(); + var markdownDocument = Markdown.Parse(value, pipeline); - Assert.AreEqual(1, markdownDocument.Count); - } + Assert.AreEqual(1, markdownDocument.Count); + } - [TestCase(" l\n\np")] - [TestCase(" l\n\n\np")] - [TestCase(" l\n\n\n\np")] - public void TestParagraph(string value) - { - RoundTrip(value); - } + [TestCase(" l\n\np")] + [TestCase(" l\n\n\np")] + [TestCase(" l\n\n\n\np")] + public void TestParagraph(string value) + { + RoundTrip(value); } } diff --git a/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs b/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs index c569764bc..a272df0d7 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs @@ -1,214 +1,212 @@ -using NUnit.Framework; using static Markdig.Tests.TestRoundtrip; -namespace Markdig.Tests.RoundtripSpecs +namespace Markdig.Tests.RoundtripSpecs; + +[TestFixture] +public class TestLinkReferenceDefinition { - [TestFixture] - public class TestLinkReferenceDefinition + [TestCase(@"[a]: /r")] + [TestCase(@" [a]: /r")] + [TestCase(@" [a]: /r")] + [TestCase(@" [a]: /r")] + + [TestCase(@"[a]: /r")] + [TestCase(@" [a]: /r")] + [TestCase(@" [a]: /r")] + [TestCase(@" [a]: /r")] + + [TestCase(@"[a]: /r ")] + [TestCase(@" [a]: /r ")] + [TestCase(@" [a]: /r ")] + [TestCase(@" [a]: /r ")] + + [TestCase(@"[a]: /r ""l""")] + [TestCase(@"[a]: /r ""l""")] + [TestCase(@"[a]: /r ""l""")] + [TestCase(@"[a]: /r ""l"" ")] + [TestCase(@"[a]: /r ""l""")] + [TestCase(@"[a]: /r ""l"" ")] + + [TestCase(@" [a]: /r ""l""")] + [TestCase(@" [a]: /r ""l""")] + [TestCase(@" [a]: /r ""l""")] + [TestCase(@" [a]: /r ""l"" ")] + [TestCase(@" [a]: /r ""l""")] + [TestCase(@" [a]: /r ""l"" ")] + + [TestCase(@" [a]: /r ""l""")] + [TestCase(@" [a]: /r ""l""")] + [TestCase(@" [a]: /r ""l""")] + [TestCase(@" [a]: /r ""l"" ")] + [TestCase(@" [a]: /r ""l""")] + [TestCase(@" [a]: /r ""l"" ")] + + [TestCase(@" [a]: /r ""l""")] + [TestCase(@" [a]: /r ""l""")] + [TestCase(@" [a]: /r ""l""")] + [TestCase(@" [a]: /r ""l"" ")] + [TestCase(@" [a]: /r ""l""")] + [TestCase(@" [a]: /r ""l"" ")] + + [TestCase("[a]:\t/r")] + [TestCase("[a]:\t/r\t")] + [TestCase("[a]:\t/r\t\"l\"")] + [TestCase("[a]:\t/r\t\"l\"\t")] + + [TestCase("[a]: \t/r")] + [TestCase("[a]: \t/r\t")] + [TestCase("[a]: \t/r\t\"l\"")] + [TestCase("[a]: \t/r\t\"l\"\t")] + + [TestCase("[a]:\t /r")] + [TestCase("[a]:\t /r\t")] + [TestCase("[a]:\t /r\t\"l\"")] + [TestCase("[a]:\t /r\t\"l\"\t")] + + [TestCase("[a]: \t /r")] + [TestCase("[a]: \t /r\t")] + [TestCase("[a]: \t /r\t\"l\"")] + [TestCase("[a]: \t /r\t\"l\"\t")] + + [TestCase("[a]:\t/r \t")] + [TestCase("[a]:\t/r \t\"l\"")] + [TestCase("[a]:\t/r \t\"l\"\t")] + + [TestCase("[a]: \t/r")] + [TestCase("[a]: \t/r \t")] + [TestCase("[a]: \t/r \t\"l\"")] + [TestCase("[a]: \t/r \t\"l\"\t")] + + [TestCase("[a]:\t /r")] + [TestCase("[a]:\t /r \t")] + [TestCase("[a]:\t /r \t\"l\"")] + [TestCase("[a]:\t /r \t\"l\"\t")] + + [TestCase("[a]: \t /r")] + [TestCase("[a]: \t /r \t")] + [TestCase("[a]: \t /r \t\"l\"")] + [TestCase("[a]: \t /r \t\"l\"\t")] + + [TestCase("[a]:\t/r\t ")] + [TestCase("[a]:\t/r\t \"l\"")] + [TestCase("[a]:\t/r\t \"l\"\t")] + + [TestCase("[a]: \t/r")] + [TestCase("[a]: \t/r\t ")] + [TestCase("[a]: \t/r\t \"l\"")] + [TestCase("[a]: \t/r\t \"l\"\t")] + + [TestCase("[a]:\t /r")] + [TestCase("[a]:\t /r\t ")] + [TestCase("[a]:\t /r\t \"l\"")] + [TestCase("[a]:\t /r\t \"l\"\t")] + + [TestCase("[a]: \t /r")] + [TestCase("[a]: \t /r\t ")] + [TestCase("[a]: \t /r\t \"l\"")] + [TestCase("[a]: \t /r\t \"l\"\t")] + + [TestCase("[a]:\t/r \t ")] + [TestCase("[a]:\t/r \t \"l\"")] + [TestCase("[a]:\t/r \t \"l\"\t")] + + [TestCase("[a]: \t/r")] + [TestCase("[a]: \t/r \t ")] + [TestCase("[a]: \t/r \t \"l\"")] + [TestCase("[a]: \t/r \t \"l\"\t")] + + [TestCase("[a]:\t /r")] + [TestCase("[a]:\t /r \t ")] + [TestCase("[a]:\t /r \t \"l\"")] + [TestCase("[a]:\t /r \t \"l\"\t")] + + [TestCase("[a]: \t /r")] + [TestCase("[a]: \t /r \t ")] + [TestCase("[a]: \t /r \t \"l\"")] + [TestCase("[a]: \t /r \t \"l\"\t")] + public void Test(string value) + { + RoundTrip(value); + } + + [TestCase("[a]: /r\n[b]: /r\n")] + [TestCase("[a]: /r\n[b]: /r\n[c] /r\n")] + public void TestMultiple(string value) + { + RoundTrip(value); + } + + [TestCase("[a]:\f/r\f\"l\"")] + [TestCase("[a]:\v/r\v\"l\"")] + public void TestUncommonWhitespace(string value) + { + RoundTrip(value); + } + + [TestCase("[a]:\n/r\n\"t\"")] + [TestCase("[a]:\n/r\r\"t\"")] + [TestCase("[a]:\n/r\r\n\"t\"")] + + [TestCase("[a]:\r/r\n\"t\"")] + [TestCase("[a]:\r/r\r\"t\"")] + [TestCase("[a]:\r/r\r\n\"t\"")] + + [TestCase("[a]:\r\n/r\n\"t\"")] + [TestCase("[a]:\r\n/r\r\"t\"")] + [TestCase("[a]:\r\n/r\r\n\"t\"")] + + [TestCase("[a]:\n/r\n\"t\nt\"")] + [TestCase("[a]:\n/r\n\"t\rt\"")] + [TestCase("[a]:\n/r\n\"t\r\nt\"")] + + [TestCase("[a]:\r\n /r\t \n \t \"t\r\nt\" ")] + [TestCase("[a]:\n/r\n\n[a],")] + [TestCase("[a]: /r\n[b]: /r\n\n[a],")] + public void TestNewlines(string value) + { + RoundTrip(value); + } + + [TestCase("[ a]: /r")] + [TestCase("[a ]: /r")] + [TestCase("[ a ]: /r")] + [TestCase("[ a]: /r")] + [TestCase("[ a ]: /r")] + [TestCase("[a ]: /r")] + [TestCase("[ a ]: /r")] + [TestCase("[ a ]: /r")] + [TestCase("[a a]: /r")] + [TestCase("[a\va]: /r")] + [TestCase("[a\fa]: /r")] + [TestCase("[a\ta]: /r")] + [TestCase("[\va]: /r")] + [TestCase("[\fa]: /r")] + [TestCase("[\ta]: /r")] + [TestCase(@"[\]]: /r")] + public void TestLabel(string value) + { + RoundTrip(value); + } + + [TestCase("[a]: /r ()")] + [TestCase("[a]: /r (t)")] + [TestCase("[a]: /r ( t)")] + [TestCase("[a]: /r (t )")] + [TestCase("[a]: /r ( t )")] + + [TestCase("[a]: /r ''")] + [TestCase("[a]: /r 't'")] + [TestCase("[a]: /r ' t'")] + [TestCase("[a]: /r 't '")] + [TestCase("[a]: /r ' t '")] + public void Test_Title(string value) + { + RoundTrip(value); + } + + [TestCase("[a]: /r\n===\n[a]")] + public void TestSetextHeader(string value) { - [TestCase(@"[a]: /r")] - [TestCase(@" [a]: /r")] - [TestCase(@" [a]: /r")] - [TestCase(@" [a]: /r")] - - [TestCase(@"[a]: /r")] - [TestCase(@" [a]: /r")] - [TestCase(@" [a]: /r")] - [TestCase(@" [a]: /r")] - - [TestCase(@"[a]: /r ")] - [TestCase(@" [a]: /r ")] - [TestCase(@" [a]: /r ")] - [TestCase(@" [a]: /r ")] - - [TestCase(@"[a]: /r ""l""")] - [TestCase(@"[a]: /r ""l""")] - [TestCase(@"[a]: /r ""l""")] - [TestCase(@"[a]: /r ""l"" ")] - [TestCase(@"[a]: /r ""l""")] - [TestCase(@"[a]: /r ""l"" ")] - - [TestCase(@" [a]: /r ""l""")] - [TestCase(@" [a]: /r ""l""")] - [TestCase(@" [a]: /r ""l""")] - [TestCase(@" [a]: /r ""l"" ")] - [TestCase(@" [a]: /r ""l""")] - [TestCase(@" [a]: /r ""l"" ")] - - [TestCase(@" [a]: /r ""l""")] - [TestCase(@" [a]: /r ""l""")] - [TestCase(@" [a]: /r ""l""")] - [TestCase(@" [a]: /r ""l"" ")] - [TestCase(@" [a]: /r ""l""")] - [TestCase(@" [a]: /r ""l"" ")] - - [TestCase(@" [a]: /r ""l""")] - [TestCase(@" [a]: /r ""l""")] - [TestCase(@" [a]: /r ""l""")] - [TestCase(@" [a]: /r ""l"" ")] - [TestCase(@" [a]: /r ""l""")] - [TestCase(@" [a]: /r ""l"" ")] - - [TestCase("[a]:\t/r")] - [TestCase("[a]:\t/r\t")] - [TestCase("[a]:\t/r\t\"l\"")] - [TestCase("[a]:\t/r\t\"l\"\t")] - - [TestCase("[a]: \t/r")] - [TestCase("[a]: \t/r\t")] - [TestCase("[a]: \t/r\t\"l\"")] - [TestCase("[a]: \t/r\t\"l\"\t")] - - [TestCase("[a]:\t /r")] - [TestCase("[a]:\t /r\t")] - [TestCase("[a]:\t /r\t\"l\"")] - [TestCase("[a]:\t /r\t\"l\"\t")] - - [TestCase("[a]: \t /r")] - [TestCase("[a]: \t /r\t")] - [TestCase("[a]: \t /r\t\"l\"")] - [TestCase("[a]: \t /r\t\"l\"\t")] - - [TestCase("[a]:\t/r \t")] - [TestCase("[a]:\t/r \t\"l\"")] - [TestCase("[a]:\t/r \t\"l\"\t")] - - [TestCase("[a]: \t/r")] - [TestCase("[a]: \t/r \t")] - [TestCase("[a]: \t/r \t\"l\"")] - [TestCase("[a]: \t/r \t\"l\"\t")] - - [TestCase("[a]:\t /r")] - [TestCase("[a]:\t /r \t")] - [TestCase("[a]:\t /r \t\"l\"")] - [TestCase("[a]:\t /r \t\"l\"\t")] - - [TestCase("[a]: \t /r")] - [TestCase("[a]: \t /r \t")] - [TestCase("[a]: \t /r \t\"l\"")] - [TestCase("[a]: \t /r \t\"l\"\t")] - - [TestCase("[a]:\t/r\t ")] - [TestCase("[a]:\t/r\t \"l\"")] - [TestCase("[a]:\t/r\t \"l\"\t")] - - [TestCase("[a]: \t/r")] - [TestCase("[a]: \t/r\t ")] - [TestCase("[a]: \t/r\t \"l\"")] - [TestCase("[a]: \t/r\t \"l\"\t")] - - [TestCase("[a]:\t /r")] - [TestCase("[a]:\t /r\t ")] - [TestCase("[a]:\t /r\t \"l\"")] - [TestCase("[a]:\t /r\t \"l\"\t")] - - [TestCase("[a]: \t /r")] - [TestCase("[a]: \t /r\t ")] - [TestCase("[a]: \t /r\t \"l\"")] - [TestCase("[a]: \t /r\t \"l\"\t")] - - [TestCase("[a]:\t/r \t ")] - [TestCase("[a]:\t/r \t \"l\"")] - [TestCase("[a]:\t/r \t \"l\"\t")] - - [TestCase("[a]: \t/r")] - [TestCase("[a]: \t/r \t ")] - [TestCase("[a]: \t/r \t \"l\"")] - [TestCase("[a]: \t/r \t \"l\"\t")] - - [TestCase("[a]:\t /r")] - [TestCase("[a]:\t /r \t ")] - [TestCase("[a]:\t /r \t \"l\"")] - [TestCase("[a]:\t /r \t \"l\"\t")] - - [TestCase("[a]: \t /r")] - [TestCase("[a]: \t /r \t ")] - [TestCase("[a]: \t /r \t \"l\"")] - [TestCase("[a]: \t /r \t \"l\"\t")] - public void Test(string value) - { - RoundTrip(value); - } - - [TestCase("[a]: /r\n[b]: /r\n")] - [TestCase("[a]: /r\n[b]: /r\n[c] /r\n")] - public void TestMultiple(string value) - { - RoundTrip(value); - } - - [TestCase("[a]:\f/r\f\"l\"")] - [TestCase("[a]:\v/r\v\"l\"")] - public void TestUncommonWhitespace(string value) - { - RoundTrip(value); - } - - [TestCase("[a]:\n/r\n\"t\"")] - [TestCase("[a]:\n/r\r\"t\"")] - [TestCase("[a]:\n/r\r\n\"t\"")] - - [TestCase("[a]:\r/r\n\"t\"")] - [TestCase("[a]:\r/r\r\"t\"")] - [TestCase("[a]:\r/r\r\n\"t\"")] - - [TestCase("[a]:\r\n/r\n\"t\"")] - [TestCase("[a]:\r\n/r\r\"t\"")] - [TestCase("[a]:\r\n/r\r\n\"t\"")] - - [TestCase("[a]:\n/r\n\"t\nt\"")] - [TestCase("[a]:\n/r\n\"t\rt\"")] - [TestCase("[a]:\n/r\n\"t\r\nt\"")] - - [TestCase("[a]:\r\n /r\t \n \t \"t\r\nt\" ")] - [TestCase("[a]:\n/r\n\n[a],")] - [TestCase("[a]: /r\n[b]: /r\n\n[a],")] - public void TestNewlines(string value) - { - RoundTrip(value); - } - - [TestCase("[ a]: /r")] - [TestCase("[a ]: /r")] - [TestCase("[ a ]: /r")] - [TestCase("[ a]: /r")] - [TestCase("[ a ]: /r")] - [TestCase("[a ]: /r")] - [TestCase("[ a ]: /r")] - [TestCase("[ a ]: /r")] - [TestCase("[a a]: /r")] - [TestCase("[a\va]: /r")] - [TestCase("[a\fa]: /r")] - [TestCase("[a\ta]: /r")] - [TestCase("[\va]: /r")] - [TestCase("[\fa]: /r")] - [TestCase("[\ta]: /r")] - [TestCase(@"[\]]: /r")] - public void TestLabel(string value) - { - RoundTrip(value); - } - - [TestCase("[a]: /r ()")] - [TestCase("[a]: /r (t)")] - [TestCase("[a]: /r ( t)")] - [TestCase("[a]: /r (t )")] - [TestCase("[a]: /r ( t )")] - - [TestCase("[a]: /r ''")] - [TestCase("[a]: /r 't'")] - [TestCase("[a]: /r ' t'")] - [TestCase("[a]: /r 't '")] - [TestCase("[a]: /r ' t '")] - public void Test_Title(string value) - { - RoundTrip(value); - } - - [TestCase("[a]: /r\n===\n[a]")] - public void TestSetextHeader(string value) - { - RoundTrip(value); - } + RoundTrip(value); } } diff --git a/src/Markdig.Tests/RoundtripSpecs/TestNoBlocksFoundBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestNoBlocksFoundBlock.cs index 806292fb8..2d221c43a 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestNoBlocksFoundBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestNoBlocksFoundBlock.cs @@ -1,23 +1,21 @@ -using NUnit.Framework; using static Markdig.Tests.TestRoundtrip; -namespace Markdig.Tests.RoundtripSpecs +namespace Markdig.Tests.RoundtripSpecs; + +[TestFixture] +public class TestNoBlocksFoundBlock { - [TestFixture] - public class TestNoBlocksFoundBlock + [TestCase("\r")] + [TestCase("\n")] + [TestCase("\r\n")] + [TestCase("\t")] + [TestCase("\v")] + [TestCase("\f")] + [TestCase(" ")] + [TestCase(" ")] + [TestCase(" ")] + public void Test(string value) { - [TestCase("\r")] - [TestCase("\n")] - [TestCase("\r\n")] - [TestCase("\t")] - [TestCase("\v")] - [TestCase("\f")] - [TestCase(" ")] - [TestCase(" ")] - [TestCase(" ")] - public void Test(string value) - { - RoundTrip(value); - } + RoundTrip(value); } } diff --git a/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs b/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs index 8aab2234b..ba981d282 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs @@ -1,191 +1,189 @@ -using NUnit.Framework; using static Markdig.Tests.TestRoundtrip; -namespace Markdig.Tests.RoundtripSpecs +namespace Markdig.Tests.RoundtripSpecs; + +[TestFixture] +public class TestOrderedList { - [TestFixture] - public class TestOrderedList + [TestCase("1. i")] + [TestCase("1. i")] + [TestCase("1. i ")] + [TestCase("1. i ")] + [TestCase("1. i ")] + + [TestCase(" 1. i")] + [TestCase(" 1. i")] + [TestCase(" 1. i ")] + [TestCase(" 1. i ")] + [TestCase(" 1. i ")] + + [TestCase(" 1. i")] + [TestCase(" 1. i")] + [TestCase(" 1. i ")] + [TestCase(" 1. i ")] + [TestCase(" 1. i ")] + + [TestCase(" 1. i")] + [TestCase(" 1. i")] + [TestCase(" 1. i ")] + [TestCase(" 1. i ")] + [TestCase(" 1. i ")] + + [TestCase("1. i\n")] + [TestCase("1. i\n")] + [TestCase("1. i \n")] + [TestCase("1. i \n")] + [TestCase("1. i \n")] + + [TestCase(" 1. i\n")] + [TestCase(" 1. i\n")] + [TestCase(" 1. i \n")] + [TestCase(" 1. i \n")] + [TestCase(" 1. i \n")] + + [TestCase(" 1. i\n")] + [TestCase(" 1. i\n")] + [TestCase(" 1. i \n")] + [TestCase(" 1. i \n")] + [TestCase(" 1. i \n")] + + [TestCase(" 1. i\n")] + [TestCase(" 1. i\n")] + [TestCase(" 1. i \n")] + [TestCase(" 1. i \n")] + [TestCase(" 1. i \n")] + + [TestCase("1. i\n2. j")] + [TestCase("1. i\n2. j")] + [TestCase("1. i \n2. j")] + [TestCase("1. i \n2. j")] + [TestCase("1. i \n2. j")] + + [TestCase(" 1. i\n2. j")] + [TestCase(" 1. i\n2. j")] + [TestCase(" 1. i \n2. j")] + [TestCase(" 1. i \n2. j")] + [TestCase(" 1. i \n2. j")] + + [TestCase(" 1. i\n2. j")] + [TestCase(" 1. i\n2. j")] + [TestCase(" 1. i \n2. j")] + [TestCase(" 1. i \n2. j")] + [TestCase(" 1. i \n2. j")] + + [TestCase(" 1. i\n2. j")] + [TestCase(" 1. i\n2. j")] + [TestCase(" 1. i \n2. j")] + [TestCase(" 1. i \n2. j")] + [TestCase(" 1. i \n2. j")] + + [TestCase("1. i\n2. j\n")] + [TestCase("1. i\n2. j\n")] + [TestCase("1. i \n2. j\n")] + [TestCase("1. i \n2. j\n")] + [TestCase("1. i \n2. j\n")] + + [TestCase(" 1. i\n2. j\n")] + [TestCase(" 1. i\n2. j\n")] + [TestCase(" 1. i \n2. j\n")] + [TestCase(" 1. i \n2. j\n")] + [TestCase(" 1. i \n2. j\n")] + + [TestCase(" 1. i\n2. j\n")] + [TestCase(" 1. i\n2. j\n")] + [TestCase(" 1. i \n2. j\n")] + [TestCase(" 1. i \n2. j\n")] + [TestCase(" 1. i \n2. j\n")] + + [TestCase(" 1. i\n2. j\n")] + [TestCase(" 1. i\n2. j\n")] + [TestCase(" 1. i \n2. j\n")] + [TestCase(" 1. i \n2. j\n")] + [TestCase(" 1. i \n2. j\n")] + + [TestCase("1. i\n2. j\n3. k")] + [TestCase("1. i\n2. j\n3. k\n")] + public void Test(string value) + { + RoundTrip(value); + } + + [TestCase("10. i")] + [TestCase("11. i")] + [TestCase("10. i\n12. i")] + [TestCase("2. i\n3. i")] + public void Test_MoreThenOneStart(string value) + { + RoundTrip(value); + } + + + [TestCase("\n1. i")] + [TestCase("\r1. i")] + [TestCase("\r\n1. i")] + + [TestCase("\n1. i\n")] + [TestCase("\r1. i\n")] + [TestCase("\r\n1. i\n")] + + [TestCase("\n1. i\r")] + [TestCase("\r1. i\r")] + [TestCase("\r\n1. i\r")] + + [TestCase("\n1. i\r\n")] + [TestCase("\r1. i\r\n")] + [TestCase("\r\n1. i\r\n")] + + [TestCase("1. i\n2. i")] + [TestCase("\n1. i\n2. i")] + [TestCase("\r1. i\n2. i")] + [TestCase("\r\n1. i\n2. i")] + + [TestCase("1. i\r2. i")] + [TestCase("\n1. i\r2. i")] + [TestCase("\r1. i\r2. i")] + [TestCase("\r\n1. i\r2. i")] + + [TestCase("1. i\r\n2. i")] + [TestCase("\n1. i\r\n2. i")] + [TestCase("\r1. i\r\n2. i")] + [TestCase("\r\n1. i\r\n2. i")] + + [TestCase("1. i\n2. i\n")] + [TestCase("\n1. i\n2. i\n")] + [TestCase("\r1. i\n2. i\n")] + [TestCase("\r\n1. i\n2. i\n")] + + [TestCase("1. i\r2. i\r")] + [TestCase("\n1. i\r2. i\r")] + [TestCase("\r1. i\r2. i\r")] + [TestCase("\r\n1. i\r2. i\r")] + + [TestCase("1. i\r\n2. i\r\n")] + [TestCase("\n1. i\r\n2. i\r\n")] + [TestCase("\r1. i\r\n2. i\r\n")] + [TestCase("\r\n1. i\r\n2. i\r\n")] + public void TestNewline(string value) + { + RoundTrip(value); + } + + [TestCase("1. i\n 1. i")] + [TestCase("1. i\n 1. i\n")] + [TestCase("1. i\n 1. i\n 2. i")] + [TestCase("1. i\n 2. i\n 3. i")] + + [TestCase("1. i\n\t1. i")] + [TestCase("1. i\n\t1. i\n2. i")] + public void TestMultipleLevels(string value) + { + RoundTrip(value); + } + + [TestCase("1. c")] + [TestCase("1. c")] + public void Test_IndentedCodeBlock(string value) { - [TestCase("1. i")] - [TestCase("1. i")] - [TestCase("1. i ")] - [TestCase("1. i ")] - [TestCase("1. i ")] - - [TestCase(" 1. i")] - [TestCase(" 1. i")] - [TestCase(" 1. i ")] - [TestCase(" 1. i ")] - [TestCase(" 1. i ")] - - [TestCase(" 1. i")] - [TestCase(" 1. i")] - [TestCase(" 1. i ")] - [TestCase(" 1. i ")] - [TestCase(" 1. i ")] - - [TestCase(" 1. i")] - [TestCase(" 1. i")] - [TestCase(" 1. i ")] - [TestCase(" 1. i ")] - [TestCase(" 1. i ")] - - [TestCase("1. i\n")] - [TestCase("1. i\n")] - [TestCase("1. i \n")] - [TestCase("1. i \n")] - [TestCase("1. i \n")] - - [TestCase(" 1. i\n")] - [TestCase(" 1. i\n")] - [TestCase(" 1. i \n")] - [TestCase(" 1. i \n")] - [TestCase(" 1. i \n")] - - [TestCase(" 1. i\n")] - [TestCase(" 1. i\n")] - [TestCase(" 1. i \n")] - [TestCase(" 1. i \n")] - [TestCase(" 1. i \n")] - - [TestCase(" 1. i\n")] - [TestCase(" 1. i\n")] - [TestCase(" 1. i \n")] - [TestCase(" 1. i \n")] - [TestCase(" 1. i \n")] - - [TestCase("1. i\n2. j")] - [TestCase("1. i\n2. j")] - [TestCase("1. i \n2. j")] - [TestCase("1. i \n2. j")] - [TestCase("1. i \n2. j")] - - [TestCase(" 1. i\n2. j")] - [TestCase(" 1. i\n2. j")] - [TestCase(" 1. i \n2. j")] - [TestCase(" 1. i \n2. j")] - [TestCase(" 1. i \n2. j")] - - [TestCase(" 1. i\n2. j")] - [TestCase(" 1. i\n2. j")] - [TestCase(" 1. i \n2. j")] - [TestCase(" 1. i \n2. j")] - [TestCase(" 1. i \n2. j")] - - [TestCase(" 1. i\n2. j")] - [TestCase(" 1. i\n2. j")] - [TestCase(" 1. i \n2. j")] - [TestCase(" 1. i \n2. j")] - [TestCase(" 1. i \n2. j")] - - [TestCase("1. i\n2. j\n")] - [TestCase("1. i\n2. j\n")] - [TestCase("1. i \n2. j\n")] - [TestCase("1. i \n2. j\n")] - [TestCase("1. i \n2. j\n")] - - [TestCase(" 1. i\n2. j\n")] - [TestCase(" 1. i\n2. j\n")] - [TestCase(" 1. i \n2. j\n")] - [TestCase(" 1. i \n2. j\n")] - [TestCase(" 1. i \n2. j\n")] - - [TestCase(" 1. i\n2. j\n")] - [TestCase(" 1. i\n2. j\n")] - [TestCase(" 1. i \n2. j\n")] - [TestCase(" 1. i \n2. j\n")] - [TestCase(" 1. i \n2. j\n")] - - [TestCase(" 1. i\n2. j\n")] - [TestCase(" 1. i\n2. j\n")] - [TestCase(" 1. i \n2. j\n")] - [TestCase(" 1. i \n2. j\n")] - [TestCase(" 1. i \n2. j\n")] - - [TestCase("1. i\n2. j\n3. k")] - [TestCase("1. i\n2. j\n3. k\n")] - public void Test(string value) - { - RoundTrip(value); - } - - [TestCase("10. i")] - [TestCase("11. i")] - [TestCase("10. i\n12. i")] - [TestCase("2. i\n3. i")] - public void Test_MoreThenOneStart(string value) - { - RoundTrip(value); - } - - - [TestCase("\n1. i")] - [TestCase("\r1. i")] - [TestCase("\r\n1. i")] - - [TestCase("\n1. i\n")] - [TestCase("\r1. i\n")] - [TestCase("\r\n1. i\n")] - - [TestCase("\n1. i\r")] - [TestCase("\r1. i\r")] - [TestCase("\r\n1. i\r")] - - [TestCase("\n1. i\r\n")] - [TestCase("\r1. i\r\n")] - [TestCase("\r\n1. i\r\n")] - - [TestCase("1. i\n2. i")] - [TestCase("\n1. i\n2. i")] - [TestCase("\r1. i\n2. i")] - [TestCase("\r\n1. i\n2. i")] - - [TestCase("1. i\r2. i")] - [TestCase("\n1. i\r2. i")] - [TestCase("\r1. i\r2. i")] - [TestCase("\r\n1. i\r2. i")] - - [TestCase("1. i\r\n2. i")] - [TestCase("\n1. i\r\n2. i")] - [TestCase("\r1. i\r\n2. i")] - [TestCase("\r\n1. i\r\n2. i")] - - [TestCase("1. i\n2. i\n")] - [TestCase("\n1. i\n2. i\n")] - [TestCase("\r1. i\n2. i\n")] - [TestCase("\r\n1. i\n2. i\n")] - - [TestCase("1. i\r2. i\r")] - [TestCase("\n1. i\r2. i\r")] - [TestCase("\r1. i\r2. i\r")] - [TestCase("\r\n1. i\r2. i\r")] - - [TestCase("1. i\r\n2. i\r\n")] - [TestCase("\n1. i\r\n2. i\r\n")] - [TestCase("\r1. i\r\n2. i\r\n")] - [TestCase("\r\n1. i\r\n2. i\r\n")] - public void TestNewline(string value) - { - RoundTrip(value); - } - - [TestCase("1. i\n 1. i")] - [TestCase("1. i\n 1. i\n")] - [TestCase("1. i\n 1. i\n 2. i")] - [TestCase("1. i\n 2. i\n 3. i")] - - [TestCase("1. i\n\t1. i")] - [TestCase("1. i\n\t1. i\n2. i")] - public void TestMultipleLevels(string value) - { - RoundTrip(value); - } - - [TestCase("1. c")] - [TestCase("1. c")] - public void Test_IndentedCodeBlock(string value) - { - RoundTrip(value); - } + RoundTrip(value); } } diff --git a/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs b/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs index 3da89f1ca..3e3969e2f 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs @@ -1,247 +1,245 @@ -using NUnit.Framework; using static Markdig.Tests.TestRoundtrip; -namespace Markdig.Tests.RoundtripSpecs +namespace Markdig.Tests.RoundtripSpecs; + +[TestFixture] +public class TestParagraph { - [TestFixture] - public class TestParagraph + [TestCase("p")] + [TestCase(" p")] + [TestCase("p ")] + [TestCase(" p ")] + + [TestCase("p\np")] + [TestCase(" p\np")] + [TestCase("p \np")] + [TestCase(" p \np")] + + [TestCase("p\n p")] + [TestCase(" p\n p")] + [TestCase("p \n p")] + [TestCase(" p \n p")] + + [TestCase("p\np ")] + [TestCase(" p\np ")] + [TestCase("p \np ")] + [TestCase(" p \np ")] + + [TestCase("p\n\n p ")] + [TestCase(" p\n\n p ")] + [TestCase("p \n\n p ")] + [TestCase(" p \n\n p ")] + + [TestCase("p\n\np")] + [TestCase(" p\n\np")] + [TestCase("p \n\np")] + [TestCase(" p \n\np")] + + [TestCase("p\n\n p")] + [TestCase(" p\n\n p")] + [TestCase("p \n\n p")] + [TestCase(" p \n\n p")] + + [TestCase("p\n\np ")] + [TestCase(" p\n\np ")] + [TestCase("p \n\np ")] + [TestCase(" p \n\np ")] + + [TestCase("p\n\n p ")] + [TestCase(" p\n\n p ")] + [TestCase("p \n\n p ")] + [TestCase(" p \n\n p ")] + + [TestCase("\np")] + [TestCase("\n p")] + [TestCase("\np ")] + [TestCase("\n p ")] + + [TestCase("\np\np")] + [TestCase("\n p\np")] + [TestCase("\np \np")] + [TestCase("\n p \np")] + + [TestCase("\np\n p")] + [TestCase("\n p\n p")] + [TestCase("\np \n p")] + [TestCase("\n p \n p")] + + [TestCase("\np\np ")] + [TestCase("\n p\np ")] + [TestCase("\np \np ")] + [TestCase("\n p \np ")] + + [TestCase("\np\n\n p ")] + [TestCase("\n p\n\n p ")] + [TestCase("\np \n\n p ")] + [TestCase("\n p \n\n p ")] + + [TestCase("\np\n\np")] + [TestCase("\n p\n\np")] + [TestCase("\np \n\np")] + [TestCase("\n p \n\np")] + + [TestCase("\np\n\n p")] + [TestCase("\n p\n\n p")] + [TestCase("\np \n\n p")] + [TestCase("\n p \n\n p")] + + [TestCase("\np\n\np ")] + [TestCase("\n p\n\np ")] + [TestCase("\np \n\np ")] + [TestCase("\n p \n\np ")] + + [TestCase("\np\n\n p ")] + [TestCase("\n p\n\n p ")] + [TestCase("\np \n\n p ")] + [TestCase("\n p \n\n p ")] + + [TestCase("p p")] + [TestCase("p\tp")] + [TestCase("p \tp")] + [TestCase("p \t p")] + [TestCase("p \tp")] + + // special cases + [TestCase(" p \n\n\n\n p \n\n")] + [TestCase("\n\np")] + [TestCase("p\n")] + [TestCase("p\n\n")] + [TestCase("p\np\n p")] + [TestCase("p\np\n p\n p")] + public void Test(string value) + { + RoundTrip(value); + } + + + [TestCase("\n")] + [TestCase("\r\n")] + [TestCase("\r")] + + [TestCase("p\n")] + [TestCase("p\r")] + [TestCase("p\r\n")] + + [TestCase("p\np")] + [TestCase("p\rp")] + [TestCase("p\r\np")] + + [TestCase("p\np\n")] + [TestCase("p\rp\n")] + [TestCase("p\r\np\n")] + + [TestCase("p\np\r")] + [TestCase("p\rp\r")] + [TestCase("p\r\np\r")] + + [TestCase("p\np\r\n")] + [TestCase("p\rp\r\n")] + [TestCase("p\r\np\r\n")] + + [TestCase("\np\n")] + [TestCase("\np\r")] + [TestCase("\np\r\n")] + + [TestCase("\np\np")] + [TestCase("\np\rp")] + [TestCase("\np\r\np")] + + [TestCase("\np\np\n")] + [TestCase("\np\rp\n")] + [TestCase("\np\r\np\n")] + + [TestCase("\np\np\r")] + [TestCase("\np\rp\r")] + [TestCase("\np\r\np\r")] + + [TestCase("\np\np\r\n")] + [TestCase("\np\rp\r\n")] + [TestCase("\np\r\np\r\n")] + + [TestCase("\rp\n")] + [TestCase("\rp\r")] + [TestCase("\rp\r\n")] + + [TestCase("\rp\np")] + [TestCase("\rp\rp")] + [TestCase("\rp\r\np")] + + [TestCase("\rp\np\n")] + [TestCase("\rp\rp\n")] + [TestCase("\rp\r\np\n")] + + [TestCase("\rp\np\r")] + [TestCase("\rp\rp\r")] + [TestCase("\rp\r\np\r")] + + [TestCase("\rp\np\r\n")] + [TestCase("\rp\rp\r\n")] + [TestCase("\rp\r\np\r\n")] + + [TestCase("\r\np\n")] + [TestCase("\r\np\r")] + [TestCase("\r\np\r\n")] + + [TestCase("\r\np\np")] + [TestCase("\r\np\rp")] + [TestCase("\r\np\r\np")] + + [TestCase("\r\np\np\n")] + [TestCase("\r\np\rp\n")] + [TestCase("\r\np\r\np\n")] + + [TestCase("\r\np\np\r")] + [TestCase("\r\np\rp\r")] + [TestCase("\r\np\r\np\r")] + + [TestCase("\r\np\np\r\n")] + [TestCase("\r\np\rp\r\n")] + [TestCase("\r\np\r\np\r\n")] + + [TestCase("p\n")] + [TestCase("p\n\n")] + [TestCase("p\n\n\n")] + [TestCase("p\n\n\n\n")] + public void TestNewline(string value) + { + RoundTrip(value); + } + + [TestCase(" \n")] + [TestCase(" \r")] + [TestCase(" \r\n")] + + [TestCase(" \np")] + [TestCase(" \rp")] + [TestCase(" \r\np")] + + [TestCase(" \np")] + [TestCase(" \rp")] + [TestCase(" \r\np")] + + [TestCase(" \np")] + [TestCase(" \rp")] + [TestCase(" \r\np")] + + [TestCase(" \n ")] + [TestCase(" \r ")] + [TestCase(" \r\n ")] + + [TestCase(" \np ")] + [TestCase(" \rp ")] + [TestCase(" \r\np ")] + + [TestCase(" \np ")] + [TestCase(" \rp ")] + [TestCase(" \r\np ")] + + [TestCase(" \np ")] + [TestCase(" \rp ")] + [TestCase(" \r\np ")] + public void Test_WhitespaceWithNewline(string value) { - [TestCase("p")] - [TestCase(" p")] - [TestCase("p ")] - [TestCase(" p ")] - - [TestCase("p\np")] - [TestCase(" p\np")] - [TestCase("p \np")] - [TestCase(" p \np")] - - [TestCase("p\n p")] - [TestCase(" p\n p")] - [TestCase("p \n p")] - [TestCase(" p \n p")] - - [TestCase("p\np ")] - [TestCase(" p\np ")] - [TestCase("p \np ")] - [TestCase(" p \np ")] - - [TestCase("p\n\n p ")] - [TestCase(" p\n\n p ")] - [TestCase("p \n\n p ")] - [TestCase(" p \n\n p ")] - - [TestCase("p\n\np")] - [TestCase(" p\n\np")] - [TestCase("p \n\np")] - [TestCase(" p \n\np")] - - [TestCase("p\n\n p")] - [TestCase(" p\n\n p")] - [TestCase("p \n\n p")] - [TestCase(" p \n\n p")] - - [TestCase("p\n\np ")] - [TestCase(" p\n\np ")] - [TestCase("p \n\np ")] - [TestCase(" p \n\np ")] - - [TestCase("p\n\n p ")] - [TestCase(" p\n\n p ")] - [TestCase("p \n\n p ")] - [TestCase(" p \n\n p ")] - - [TestCase("\np")] - [TestCase("\n p")] - [TestCase("\np ")] - [TestCase("\n p ")] - - [TestCase("\np\np")] - [TestCase("\n p\np")] - [TestCase("\np \np")] - [TestCase("\n p \np")] - - [TestCase("\np\n p")] - [TestCase("\n p\n p")] - [TestCase("\np \n p")] - [TestCase("\n p \n p")] - - [TestCase("\np\np ")] - [TestCase("\n p\np ")] - [TestCase("\np \np ")] - [TestCase("\n p \np ")] - - [TestCase("\np\n\n p ")] - [TestCase("\n p\n\n p ")] - [TestCase("\np \n\n p ")] - [TestCase("\n p \n\n p ")] - - [TestCase("\np\n\np")] - [TestCase("\n p\n\np")] - [TestCase("\np \n\np")] - [TestCase("\n p \n\np")] - - [TestCase("\np\n\n p")] - [TestCase("\n p\n\n p")] - [TestCase("\np \n\n p")] - [TestCase("\n p \n\n p")] - - [TestCase("\np\n\np ")] - [TestCase("\n p\n\np ")] - [TestCase("\np \n\np ")] - [TestCase("\n p \n\np ")] - - [TestCase("\np\n\n p ")] - [TestCase("\n p\n\n p ")] - [TestCase("\np \n\n p ")] - [TestCase("\n p \n\n p ")] - - [TestCase("p p")] - [TestCase("p\tp")] - [TestCase("p \tp")] - [TestCase("p \t p")] - [TestCase("p \tp")] - - // special cases - [TestCase(" p \n\n\n\n p \n\n")] - [TestCase("\n\np")] - [TestCase("p\n")] - [TestCase("p\n\n")] - [TestCase("p\np\n p")] - [TestCase("p\np\n p\n p")] - public void Test(string value) - { - RoundTrip(value); - } - - - [TestCase("\n")] - [TestCase("\r\n")] - [TestCase("\r")] - - [TestCase("p\n")] - [TestCase("p\r")] - [TestCase("p\r\n")] - - [TestCase("p\np")] - [TestCase("p\rp")] - [TestCase("p\r\np")] - - [TestCase("p\np\n")] - [TestCase("p\rp\n")] - [TestCase("p\r\np\n")] - - [TestCase("p\np\r")] - [TestCase("p\rp\r")] - [TestCase("p\r\np\r")] - - [TestCase("p\np\r\n")] - [TestCase("p\rp\r\n")] - [TestCase("p\r\np\r\n")] - - [TestCase("\np\n")] - [TestCase("\np\r")] - [TestCase("\np\r\n")] - - [TestCase("\np\np")] - [TestCase("\np\rp")] - [TestCase("\np\r\np")] - - [TestCase("\np\np\n")] - [TestCase("\np\rp\n")] - [TestCase("\np\r\np\n")] - - [TestCase("\np\np\r")] - [TestCase("\np\rp\r")] - [TestCase("\np\r\np\r")] - - [TestCase("\np\np\r\n")] - [TestCase("\np\rp\r\n")] - [TestCase("\np\r\np\r\n")] - - [TestCase("\rp\n")] - [TestCase("\rp\r")] - [TestCase("\rp\r\n")] - - [TestCase("\rp\np")] - [TestCase("\rp\rp")] - [TestCase("\rp\r\np")] - - [TestCase("\rp\np\n")] - [TestCase("\rp\rp\n")] - [TestCase("\rp\r\np\n")] - - [TestCase("\rp\np\r")] - [TestCase("\rp\rp\r")] - [TestCase("\rp\r\np\r")] - - [TestCase("\rp\np\r\n")] - [TestCase("\rp\rp\r\n")] - [TestCase("\rp\r\np\r\n")] - - [TestCase("\r\np\n")] - [TestCase("\r\np\r")] - [TestCase("\r\np\r\n")] - - [TestCase("\r\np\np")] - [TestCase("\r\np\rp")] - [TestCase("\r\np\r\np")] - - [TestCase("\r\np\np\n")] - [TestCase("\r\np\rp\n")] - [TestCase("\r\np\r\np\n")] - - [TestCase("\r\np\np\r")] - [TestCase("\r\np\rp\r")] - [TestCase("\r\np\r\np\r")] - - [TestCase("\r\np\np\r\n")] - [TestCase("\r\np\rp\r\n")] - [TestCase("\r\np\r\np\r\n")] - - [TestCase("p\n")] - [TestCase("p\n\n")] - [TestCase("p\n\n\n")] - [TestCase("p\n\n\n\n")] - public void TestNewline(string value) - { - RoundTrip(value); - } - - [TestCase(" \n")] - [TestCase(" \r")] - [TestCase(" \r\n")] - - [TestCase(" \np")] - [TestCase(" \rp")] - [TestCase(" \r\np")] - - [TestCase(" \np")] - [TestCase(" \rp")] - [TestCase(" \r\np")] - - [TestCase(" \np")] - [TestCase(" \rp")] - [TestCase(" \r\np")] - - [TestCase(" \n ")] - [TestCase(" \r ")] - [TestCase(" \r\n ")] - - [TestCase(" \np ")] - [TestCase(" \rp ")] - [TestCase(" \r\np ")] - - [TestCase(" \np ")] - [TestCase(" \rp ")] - [TestCase(" \r\np ")] - - [TestCase(" \np ")] - [TestCase(" \rp ")] - [TestCase(" \r\np ")] - public void Test_WhitespaceWithNewline(string value) - { - RoundTrip(value); - } + RoundTrip(value); } } diff --git a/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs index 6820db651..cdba1af47 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs @@ -1,286 +1,284 @@ -using NUnit.Framework; using static Markdig.Tests.TestRoundtrip; -namespace Markdig.Tests.RoundtripSpecs +namespace Markdig.Tests.RoundtripSpecs; + +[TestFixture] +public class TestQuoteBlock { - [TestFixture] - public class TestQuoteBlock - { - [TestCase(">q")] - [TestCase(" >q")] - [TestCase(" >q")] - [TestCase(" >q")] - [TestCase("> q")] - [TestCase(" > q")] - [TestCase(" > q")] - [TestCase(" > q")] - [TestCase("> q")] - [TestCase(" > q")] - [TestCase(" > q")] - [TestCase(" > q")] + [TestCase(">q")] + [TestCase(" >q")] + [TestCase(" >q")] + [TestCase(" >q")] + [TestCase("> q")] + [TestCase(" > q")] + [TestCase(" > q")] + [TestCase(" > q")] + [TestCase("> q")] + [TestCase(" > q")] + [TestCase(" > q")] + [TestCase(" > q")] - [TestCase(">q\n>q")] - [TestCase(">q\n >q")] - [TestCase(">q\n >q")] - [TestCase(">q\n >q")] - [TestCase(">q\n> q")] - [TestCase(">q\n > q")] - [TestCase(">q\n > q")] - [TestCase(">q\n > q")] - [TestCase(">q\n> q")] - [TestCase(">q\n > q")] - [TestCase(">q\n > q")] - [TestCase(">q\n > q")] + [TestCase(">q\n>q")] + [TestCase(">q\n >q")] + [TestCase(">q\n >q")] + [TestCase(">q\n >q")] + [TestCase(">q\n> q")] + [TestCase(">q\n > q")] + [TestCase(">q\n > q")] + [TestCase(">q\n > q")] + [TestCase(">q\n> q")] + [TestCase(">q\n > q")] + [TestCase(">q\n > q")] + [TestCase(">q\n > q")] - [TestCase(" >q\n>q")] - [TestCase(" >q\n >q")] - [TestCase(" >q\n >q")] - [TestCase(" >q\n >q")] - [TestCase(" >q\n> q")] - [TestCase(" >q\n > q")] - [TestCase(" >q\n > q")] - [TestCase(" >q\n > q")] - [TestCase(" >q\n> q")] - [TestCase(" >q\n > q")] - [TestCase(" >q\n > q")] - [TestCase(" >q\n > q")] + [TestCase(" >q\n>q")] + [TestCase(" >q\n >q")] + [TestCase(" >q\n >q")] + [TestCase(" >q\n >q")] + [TestCase(" >q\n> q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n> q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n > q")] - [TestCase(" >q\n>q")] - [TestCase(" >q\n >q")] - [TestCase(" >q\n >q")] - [TestCase(" >q\n >q")] - [TestCase(" >q\n> q")] - [TestCase(" >q\n > q")] - [TestCase(" >q\n > q")] - [TestCase(" >q\n > q")] - [TestCase(" >q\n> q")] - [TestCase(" >q\n > q")] - [TestCase(" >q\n > q")] - [TestCase(" >q\n > q")] + [TestCase(" >q\n>q")] + [TestCase(" >q\n >q")] + [TestCase(" >q\n >q")] + [TestCase(" >q\n >q")] + [TestCase(" >q\n> q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n> q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n > q")] - [TestCase("> q\n>q")] - [TestCase("> q\n >q")] - [TestCase("> q\n >q")] - [TestCase("> q\n >q")] - [TestCase("> q\n> q")] - [TestCase("> q\n > q")] - [TestCase("> q\n > q")] - [TestCase("> q\n > q")] - [TestCase("> q\n> q")] - [TestCase("> q\n > q")] - [TestCase("> q\n > q")] - [TestCase("> q\n > q")] + [TestCase("> q\n>q")] + [TestCase("> q\n >q")] + [TestCase("> q\n >q")] + [TestCase("> q\n >q")] + [TestCase("> q\n> q")] + [TestCase("> q\n > q")] + [TestCase("> q\n > q")] + [TestCase("> q\n > q")] + [TestCase("> q\n> q")] + [TestCase("> q\n > q")] + [TestCase("> q\n > q")] + [TestCase("> q\n > q")] - [TestCase(" > q\n>q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n> q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n> q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] + [TestCase(" > q\n>q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] - [TestCase(" > q\n>q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n> q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n> q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] + [TestCase(" > q\n>q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] - [TestCase(" > q\n>q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n> q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n> q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] + [TestCase(" > q\n>q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] - [TestCase("> q\n>q")] - [TestCase("> q\n >q")] - [TestCase("> q\n >q")] - [TestCase("> q\n >q")] - [TestCase("> q\n> q")] - [TestCase("> q\n > q")] - [TestCase("> q\n > q")] - [TestCase("> q\n > q")] - [TestCase("> q\n> q")] - [TestCase("> q\n > q")] - [TestCase("> q\n > q")] - [TestCase("> q\n > q")] + [TestCase("> q\n>q")] + [TestCase("> q\n >q")] + [TestCase("> q\n >q")] + [TestCase("> q\n >q")] + [TestCase("> q\n> q")] + [TestCase("> q\n > q")] + [TestCase("> q\n > q")] + [TestCase("> q\n > q")] + [TestCase("> q\n> q")] + [TestCase("> q\n > q")] + [TestCase("> q\n > q")] + [TestCase("> q\n > q")] - [TestCase(" > q\n>q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n> q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n> q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] + [TestCase(" > q\n>q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] - [TestCase(" > q\n>q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n> q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n> q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] + [TestCase(" > q\n>q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] - [TestCase(" > q\n>q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n> q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n> q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] + [TestCase(" > q\n>q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] - [TestCase(">q\n>q\n>q")] - [TestCase(">q\n>\n>q")] - [TestCase(">q\np\n>q")] - [TestCase(">q\n>\n>\n>q")] - [TestCase(">q\n>\n>\n>\n>q")] - [TestCase(">q\n>\n>q\n>\n>q")] - [TestCase("p\n\n> **q**\n>p\n")] + [TestCase(">q\n>q\n>q")] + [TestCase(">q\n>\n>q")] + [TestCase(">q\np\n>q")] + [TestCase(">q\n>\n>\n>q")] + [TestCase(">q\n>\n>\n>\n>q")] + [TestCase(">q\n>\n>q\n>\n>q")] + [TestCase("p\n\n> **q**\n>p\n")] - [TestCase("> q\np\n> q")] // lazy - [TestCase("> q\n> q\np")] // lazy + [TestCase("> q\np\n> q")] // lazy + [TestCase("> q\n> q\np")] // lazy - [TestCase(">>q")] - [TestCase(" > > q")] + [TestCase(">>q")] + [TestCase(" > > q")] - [TestCase("> **q**\n>p\n")] - [TestCase("> **q**")] - public void Test(string value) - { - RoundTrip(value); - } + [TestCase("> **q**\n>p\n")] + [TestCase("> **q**")] + public void Test(string value) + { + RoundTrip(value); + } - [TestCase("> q")] // 5 - [TestCase("> q")] // 6 - [TestCase(" > q")] //5 - [TestCase(" > q")] //6 - [TestCase(" > \tq")] - [TestCase("> q\n> q")] // 5, 5 - [TestCase("> q\n> q")] // 5, 6 - [TestCase("> q\n> q")] // 6, 5 - [TestCase("> q\n> q")] // 6, 6 - [TestCase("> q\n\n> 5")] // 5, 5 - public void TestIndentedCodeBlock(string value) - { - RoundTrip(value); - } + [TestCase("> q")] // 5 + [TestCase("> q")] // 6 + [TestCase(" > q")] //5 + [TestCase(" > q")] //6 + [TestCase(" > \tq")] + [TestCase("> q\n> q")] // 5, 5 + [TestCase("> q\n> q")] // 5, 6 + [TestCase("> q\n> q")] // 6, 5 + [TestCase("> q\n> q")] // 6, 6 + [TestCase("> q\n\n> 5")] // 5, 5 + public void TestIndentedCodeBlock(string value) + { + RoundTrip(value); + } - [TestCase("\n> q")] - [TestCase("\n> q\n")] - [TestCase("\n> q\n\n")] - [TestCase("> q\n\np")] - [TestCase("p\n\n> q\n\n# h")] + [TestCase("\n> q")] + [TestCase("\n> q\n")] + [TestCase("\n> q\n\n")] + [TestCase("> q\n\np")] + [TestCase("p\n\n> q\n\n# h")] - //https://github.com/lunet-io/markdig/issues/480 - //[TestCase(">\np")] - //[TestCase(">**b**\n>\n>p\n>\np\n")] - public void TestParagraph(string value) - { - RoundTrip(value); - } + //https://github.com/lunet-io/markdig/issues/480 + //[TestCase(">\np")] + //[TestCase(">**b**\n>\n>p\n>\np\n")] + public void TestParagraph(string value) + { + RoundTrip(value); + } - [TestCase("> q\n\n# h\n")] - public void TestAtxHeader(string value) - { - RoundTrip(value); - } + [TestCase("> q\n\n# h\n")] + public void TestAtxHeader(string value) + { + RoundTrip(value); + } - [TestCase(">- i")] - [TestCase("> - i")] - [TestCase(">- i\n>- i")] - [TestCase(">- >p")] - [TestCase("> - >p")] - [TestCase(">- i1\n>- i2\n")] - [TestCase("> **p** p\n>- i1\n>- i2\n")] - public void TestUnorderedList(string value) - { - RoundTrip(value); - } + [TestCase(">- i")] + [TestCase("> - i")] + [TestCase(">- i\n>- i")] + [TestCase(">- >p")] + [TestCase("> - >p")] + [TestCase(">- i1\n>- i2\n")] + [TestCase("> **p** p\n>- i1\n>- i2\n")] + public void TestUnorderedList(string value) + { + RoundTrip(value); + } - [TestCase("> *q*\n>p\n")] - [TestCase("> *q*")] - public void TestEmphasis(string value) - { - RoundTrip(value); - } + [TestCase("> *q*\n>p\n")] + [TestCase("> *q*")] + public void TestEmphasis(string value) + { + RoundTrip(value); + } - [TestCase("> **q**\n>p\n")] - [TestCase("> **q**")] - public void TestStrongEmphasis(string value) - { - RoundTrip(value); - } + [TestCase("> **q**\n>p\n")] + [TestCase("> **q**")] + public void TestStrongEmphasis(string value) + { + RoundTrip(value); + } - [TestCase(">p\n")] - [TestCase(">p\r")] - [TestCase(">p\r\n")] + [TestCase(">p\n")] + [TestCase(">p\r")] + [TestCase(">p\r\n")] - [TestCase(">p\n>p")] - [TestCase(">p\r>p")] - [TestCase(">p\r\n>p")] + [TestCase(">p\n>p")] + [TestCase(">p\r>p")] + [TestCase(">p\r\n>p")] - [TestCase(">p\n>p\n")] - [TestCase(">p\r>p\n")] - [TestCase(">p\r\n>p\n")] + [TestCase(">p\n>p\n")] + [TestCase(">p\r>p\n")] + [TestCase(">p\r\n>p\n")] - [TestCase(">p\n>p\r")] - [TestCase(">p\r>p\r")] - [TestCase(">p\r\n>p\r")] + [TestCase(">p\n>p\r")] + [TestCase(">p\r>p\r")] + [TestCase(">p\r\n>p\r")] - [TestCase(">p\n>p\r\n")] - [TestCase(">p\r>p\r\n")] - [TestCase(">p\r\n>p\r\n")] - public void TestNewline(string value) - { - RoundTrip(value); - } + [TestCase(">p\n>p\r\n")] + [TestCase(">p\r>p\r\n")] + [TestCase(">p\r\n>p\r\n")] + public void TestNewline(string value) + { + RoundTrip(value); + } - [TestCase(">\n>q")] - [TestCase(">\n>\n>q")] - [TestCase(">q\n>\n>q")] - [TestCase(">q\n>\n>\n>q")] - [TestCase(">q\n> \n>q")] - [TestCase(">q\n> \n>q")] - [TestCase(">q\n> \n>q")] - [TestCase(">q\n>\t\n>q")] - [TestCase(">q\n>\v\n>q")] - [TestCase(">q\n>\f\n>q")] - public void TestEmptyLines(string value) - { - RoundTrip(value); - } + [TestCase(">\n>q")] + [TestCase(">\n>\n>q")] + [TestCase(">q\n>\n>q")] + [TestCase(">q\n>\n>\n>q")] + [TestCase(">q\n> \n>q")] + [TestCase(">q\n> \n>q")] + [TestCase(">q\n> \n>q")] + [TestCase(">q\n>\t\n>q")] + [TestCase(">q\n>\v\n>q")] + [TestCase(">q\n>\f\n>q")] + public void TestEmptyLines(string value) + { + RoundTrip(value); } } diff --git a/src/Markdig.Tests/RoundtripSpecs/TestSetextHeading.cs b/src/Markdig.Tests/RoundtripSpecs/TestSetextHeading.cs index f367c368f..2fc6165ce 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestSetextHeading.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestSetextHeading.cs @@ -1,53 +1,51 @@ -using NUnit.Framework; using static Markdig.Tests.TestRoundtrip; -namespace Markdig.Tests.RoundtripSpecs +namespace Markdig.Tests.RoundtripSpecs; + +[TestFixture] +public class TestSetextHeading { - [TestFixture] - public class TestSetextHeading + [TestCase("h1\n===")] //3 + [TestCase("h1\n ===")] //3 + [TestCase("h1\n ===")] //3 + [TestCase("h1\n ===")] //3 + [TestCase("h1\n=== ")] //3 + [TestCase("h1 \n===")] //3 + [TestCase("h1\\\n===")] //3 + [TestCase("h1\n === ")] //3 + [TestCase("h1\nh1 l2\n===")] //3 + [TestCase("h1\n====")] // 4 + [TestCase("h1\n ====")] // 4 + [TestCase("h1\n==== ")] // 4 + [TestCase("h1\n ==== ")] // 4 + [TestCase("h1\n===\nh1\n===")] //3 + [TestCase("\\>h1\n===")] //3 + public void Test(string value) { - [TestCase("h1\n===")] //3 - [TestCase("h1\n ===")] //3 - [TestCase("h1\n ===")] //3 - [TestCase("h1\n ===")] //3 - [TestCase("h1\n=== ")] //3 - [TestCase("h1 \n===")] //3 - [TestCase("h1\\\n===")] //3 - [TestCase("h1\n === ")] //3 - [TestCase("h1\nh1 l2\n===")] //3 - [TestCase("h1\n====")] // 4 - [TestCase("h1\n ====")] // 4 - [TestCase("h1\n==== ")] // 4 - [TestCase("h1\n ==== ")] // 4 - [TestCase("h1\n===\nh1\n===")] //3 - [TestCase("\\>h1\n===")] //3 - public void Test(string value) - { - RoundTrip(value); - } + RoundTrip(value); + } - [TestCase("h1\r===")] - [TestCase("h1\n===")] - [TestCase("h1\r\n===")] + [TestCase("h1\r===")] + [TestCase("h1\n===")] + [TestCase("h1\r\n===")] - [TestCase("h1\r===\r")] - [TestCase("h1\n===\r")] - [TestCase("h1\r\n===\r")] + [TestCase("h1\r===\r")] + [TestCase("h1\n===\r")] + [TestCase("h1\r\n===\r")] - [TestCase("h1\r===\n")] - [TestCase("h1\n===\n")] - [TestCase("h1\r\n===\n")] + [TestCase("h1\r===\n")] + [TestCase("h1\n===\n")] + [TestCase("h1\r\n===\n")] - [TestCase("h1\r===\r\n")] - [TestCase("h1\n===\r\n")] - [TestCase("h1\r\n===\r\n")] + [TestCase("h1\r===\r\n")] + [TestCase("h1\n===\r\n")] + [TestCase("h1\r\n===\r\n")] - [TestCase("h1\n===\n\n\nh2---\n\n")] - [TestCase("h1\r===\r\r\rh2---\r\r")] - [TestCase("h1\r\n===\r\n\r\n\r\nh2---\r\n\r\n")] - public void TestNewline(string value) - { - RoundTrip(value); - } + [TestCase("h1\n===\n\n\nh2---\n\n")] + [TestCase("h1\r===\r\r\rh2---\r\r")] + [TestCase("h1\r\n===\r\n\r\n\r\nh2---\r\n\r\n")] + public void TestNewline(string value) + { + RoundTrip(value); } } diff --git a/src/Markdig.Tests/RoundtripSpecs/TestThematicBreak.cs b/src/Markdig.Tests/RoundtripSpecs/TestThematicBreak.cs index e1eaf1576..58f2cdcc5 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestThematicBreak.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestThematicBreak.cs @@ -1,53 +1,51 @@ -using NUnit.Framework; using static Markdig.Tests.TestRoundtrip; -namespace Markdig.Tests.RoundtripSpecs +namespace Markdig.Tests.RoundtripSpecs; + +[TestFixture] +public class TestThematicBreak { - [TestFixture] - public class TestThematicBreak + [TestCase("---")] + [TestCase(" ---")] + [TestCase(" ---")] + [TestCase(" ---")] + [TestCase("--- ")] + [TestCase(" --- ")] + [TestCase(" --- ")] + [TestCase(" --- ")] + [TestCase("- - -")] + [TestCase(" - - -")] + [TestCase(" - - - ")] + [TestCase("-- -")] + [TestCase("---\n")] + [TestCase("---\n\n")] + [TestCase("---\np")] + [TestCase("---\n\np")] + [TestCase("---\n# h")] + [TestCase("p\n\n---")] + // Note: "p\n---" is parsed as setext heading + public void Test(string value) { - [TestCase("---")] - [TestCase(" ---")] - [TestCase(" ---")] - [TestCase(" ---")] - [TestCase("--- ")] - [TestCase(" --- ")] - [TestCase(" --- ")] - [TestCase(" --- ")] - [TestCase("- - -")] - [TestCase(" - - -")] - [TestCase(" - - - ")] - [TestCase("-- -")] - [TestCase("---\n")] - [TestCase("---\n\n")] - [TestCase("---\np")] - [TestCase("---\n\np")] - [TestCase("---\n# h")] - [TestCase("p\n\n---")] - // Note: "p\n---" is parsed as setext heading - public void Test(string value) - { - RoundTrip(value); - } + RoundTrip(value); + } - [TestCase("\n---")] - [TestCase("\r---")] - [TestCase("\r\n---")] + [TestCase("\n---")] + [TestCase("\r---")] + [TestCase("\r\n---")] - [TestCase("\n---\n")] - [TestCase("\r---\n")] - [TestCase("\r\n---\n")] + [TestCase("\n---\n")] + [TestCase("\r---\n")] + [TestCase("\r\n---\n")] - [TestCase("\n---\r")] - [TestCase("\r---\r")] - [TestCase("\r\n---\r")] + [TestCase("\n---\r")] + [TestCase("\r---\r")] + [TestCase("\r\n---\r")] - [TestCase("\n---\r\n")] - [TestCase("\r---\r\n")] - [TestCase("\r\n---\r\n")] - public void TestNewline(string value) - { - RoundTrip(value); - } + [TestCase("\n---\r\n")] + [TestCase("\r---\r\n")] + [TestCase("\r\n---\r\n")] + public void TestNewline(string value) + { + RoundTrip(value); } } diff --git a/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs b/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs index 5a4d17d2b..f35a2e8bd 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs @@ -1,183 +1,181 @@ -using NUnit.Framework; using static Markdig.Tests.TestRoundtrip; -namespace Markdig.Tests.RoundtripSpecs +namespace Markdig.Tests.RoundtripSpecs; + +[TestFixture] +public class TestUnorderedList { - [TestFixture] - public class TestUnorderedList + // i = item + [TestCase("- i1")] + [TestCase("- i1 ")] + [TestCase("- i1\n")] + [TestCase("- i1\n\n")] + [TestCase("- i1\n- i2")] + [TestCase("- i1\n - i2")] + [TestCase("- i1\n - i1.1\n - i1.2")] + [TestCase("- i1 \n- i2 \n")] + [TestCase("- i1 \n- i2 \n")] + [TestCase(" - i1")] + [TestCase(" - i1")] + [TestCase(" - i1")] + [TestCase("- i1\n\n- i1")] + [TestCase("- i1\n\n\n- i1")] + [TestCase("- i1\n - i1.1\n - i1.1.1\n")] + + [TestCase("-\ti1")] + [TestCase("-\ti1\n-\ti2")] + [TestCase("-\ti1\n- i2\n-\ti3")] + public void Test(string value) + { + RoundTrip(value); + } + + [TestCase("- > q")] + [TestCase(" - > q")] + [TestCase(" - > q")] + [TestCase(" - > q")] + [TestCase("- > q")] + [TestCase(" - > q")] + [TestCase(" - > q")] + [TestCase(" - > q")] + [TestCase("- > q")] + [TestCase(" - > q")] + [TestCase(" - > q")] + [TestCase(" - > q")] + [TestCase("- > q")] + [TestCase(" - > q")] + [TestCase(" - > q")] + [TestCase(" - > q")] + [TestCase(" - > q1\n - > q2")] + public void TestBlockQuote(string value) + { + RoundTrip(value); + } + + [TestCase("- i1\n\np\n")] // TODO: listblock should render newline, apparently last paragraph of last listitem dont have newline + [TestCase("- i1\n\n\np\n")] + [TestCase("- i1\n\np")] + [TestCase("- i1\n\np\n")] + public void TestParagraph(string value) + { + RoundTrip(value); + } + + [TestCase("- i1\n\n---\n")] + [TestCase("- i1\n\n\n---\n")] + public void TestThematicBreak(string value) + { + RoundTrip(value); + } + + [TestCase("- c")] // 5 + [TestCase("- c\n c")] // 5, 6 + [TestCase(" - c\n c")] // 5, 6 + [TestCase(" - c\n c")] // 5, 7 + [TestCase("- c\n c")] // 6, 6 + [TestCase(" - c\n c")] // 6, 6 + [TestCase(" - c\n c")] // 6, 7 + public void TestIndentedCodeBlock(string value) + { + RoundTrip(value); + } + + [TestCase("- ```a```")] + [TestCase("- ```\n a\n```")] + [TestCase("- i1\n - i1.1\n ```\n c\n ```")] + [TestCase("- i1\n - i1.1\n ```\nc\n```")] + [TestCase("- i1\n - i1.1\n ```\nc\n```\n")] + public void TestFencedCodeBlock(string value) + { + RoundTrip(value); + } + + [TestCase("\n- i")] + [TestCase("\r- i")] + [TestCase("\r\n- i")] + + [TestCase("\n- i\n")] + [TestCase("\r- i\n")] + [TestCase("\r\n- i\n")] + + [TestCase("\n- i\r")] + [TestCase("\r- i\r")] + [TestCase("\r\n- i\r")] + + [TestCase("\n- i\r\n")] + [TestCase("\r- i\r\n")] + [TestCase("\r\n- i\r\n")] + + [TestCase("- i\n- j")] + [TestCase("- i\r- j")] + [TestCase("- i\r\n- j")] + + [TestCase("\n- i\n- j")] + [TestCase("\n- i\r- j")] + [TestCase("\n- i\r\n- j")] + + [TestCase("\r- i\n- j")] + [TestCase("\r- i\r- j")] + [TestCase("\r- i\r\n- j")] + + [TestCase("\r\n- i\n- j")] + [TestCase("\r\n- i\r- j")] + [TestCase("\r\n- i\r\n- j")] + + [TestCase("- i\n- j\n")] + [TestCase("- i\r- j\n")] + [TestCase("- i\r\n- j\n")] + + [TestCase("\n- i\n- j\n")] + [TestCase("\n- i\r- j\n")] + [TestCase("\n- i\r\n- j\n")] + + [TestCase("\r- i\n- j\n")] + [TestCase("\r- i\r- j\n")] + [TestCase("\r- i\r\n- j\n")] + + [TestCase("\r\n- i\n- j\n")] + [TestCase("\r\n- i\r- j\n")] + [TestCase("\r\n- i\r\n- j\n")] + + [TestCase("- i\n- j\r")] + [TestCase("- i\r- j\r")] + [TestCase("- i\r\n- j\r")] + + [TestCase("\n- i\n- j\r")] + [TestCase("\n- i\r- j\r")] + [TestCase("\n- i\r\n- j\r")] + + [TestCase("\r- i\n- j\r")] + [TestCase("\r- i\r- j\r")] + [TestCase("\r- i\r\n- j\r")] + + [TestCase("\r\n- i\n- j\r")] + [TestCase("\r\n- i\r- j\r")] + [TestCase("\r\n- i\r\n- j\r")] + + [TestCase("- i\n- j\r\n")] + [TestCase("- i\r- j\r\n")] + [TestCase("- i\r\n- j\r\n")] + + [TestCase("\n- i\n- j\r\n")] + [TestCase("\n- i\r- j\r\n")] + [TestCase("\n- i\r\n- j\r\n")] + + [TestCase("\r- i\n- j\r\n")] + [TestCase("\r- i\r- j\r\n")] + [TestCase("\r- i\r\n- j\r\n")] + + [TestCase("\r\n- i\n- j\r\n")] + [TestCase("\r\n- i\r- j\r\n")] + [TestCase("\r\n- i\r\n- j\r\n")] + + [TestCase("- i\n")] + [TestCase("- i\n\n")] + [TestCase("- i\n\n\n")] + [TestCase("- i\n\n\n\n")] + public void TestNewline(string value) { - // i = item - [TestCase("- i1")] - [TestCase("- i1 ")] - [TestCase("- i1\n")] - [TestCase("- i1\n\n")] - [TestCase("- i1\n- i2")] - [TestCase("- i1\n - i2")] - [TestCase("- i1\n - i1.1\n - i1.2")] - [TestCase("- i1 \n- i2 \n")] - [TestCase("- i1 \n- i2 \n")] - [TestCase(" - i1")] - [TestCase(" - i1")] - [TestCase(" - i1")] - [TestCase("- i1\n\n- i1")] - [TestCase("- i1\n\n\n- i1")] - [TestCase("- i1\n - i1.1\n - i1.1.1\n")] - - [TestCase("-\ti1")] - [TestCase("-\ti1\n-\ti2")] - [TestCase("-\ti1\n- i2\n-\ti3")] - public void Test(string value) - { - RoundTrip(value); - } - - [TestCase("- > q")] - [TestCase(" - > q")] - [TestCase(" - > q")] - [TestCase(" - > q")] - [TestCase("- > q")] - [TestCase(" - > q")] - [TestCase(" - > q")] - [TestCase(" - > q")] - [TestCase("- > q")] - [TestCase(" - > q")] - [TestCase(" - > q")] - [TestCase(" - > q")] - [TestCase("- > q")] - [TestCase(" - > q")] - [TestCase(" - > q")] - [TestCase(" - > q")] - [TestCase(" - > q1\n - > q2")] - public void TestBlockQuote(string value) - { - RoundTrip(value); - } - - [TestCase("- i1\n\np\n")] // TODO: listblock should render newline, apparently last paragraph of last listitem dont have newline - [TestCase("- i1\n\n\np\n")] - [TestCase("- i1\n\np")] - [TestCase("- i1\n\np\n")] - public void TestParagraph(string value) - { - RoundTrip(value); - } - - [TestCase("- i1\n\n---\n")] - [TestCase("- i1\n\n\n---\n")] - public void TestThematicBreak(string value) - { - RoundTrip(value); - } - - [TestCase("- c")] // 5 - [TestCase("- c\n c")] // 5, 6 - [TestCase(" - c\n c")] // 5, 6 - [TestCase(" - c\n c")] // 5, 7 - [TestCase("- c\n c")] // 6, 6 - [TestCase(" - c\n c")] // 6, 6 - [TestCase(" - c\n c")] // 6, 7 - public void TestIndentedCodeBlock(string value) - { - RoundTrip(value); - } - - [TestCase("- ```a```")] - [TestCase("- ```\n a\n```")] - [TestCase("- i1\n - i1.1\n ```\n c\n ```")] - [TestCase("- i1\n - i1.1\n ```\nc\n```")] - [TestCase("- i1\n - i1.1\n ```\nc\n```\n")] - public void TestFencedCodeBlock(string value) - { - RoundTrip(value); - } - - [TestCase("\n- i")] - [TestCase("\r- i")] - [TestCase("\r\n- i")] - - [TestCase("\n- i\n")] - [TestCase("\r- i\n")] - [TestCase("\r\n- i\n")] - - [TestCase("\n- i\r")] - [TestCase("\r- i\r")] - [TestCase("\r\n- i\r")] - - [TestCase("\n- i\r\n")] - [TestCase("\r- i\r\n")] - [TestCase("\r\n- i\r\n")] - - [TestCase("- i\n- j")] - [TestCase("- i\r- j")] - [TestCase("- i\r\n- j")] - - [TestCase("\n- i\n- j")] - [TestCase("\n- i\r- j")] - [TestCase("\n- i\r\n- j")] - - [TestCase("\r- i\n- j")] - [TestCase("\r- i\r- j")] - [TestCase("\r- i\r\n- j")] - - [TestCase("\r\n- i\n- j")] - [TestCase("\r\n- i\r- j")] - [TestCase("\r\n- i\r\n- j")] - - [TestCase("- i\n- j\n")] - [TestCase("- i\r- j\n")] - [TestCase("- i\r\n- j\n")] - - [TestCase("\n- i\n- j\n")] - [TestCase("\n- i\r- j\n")] - [TestCase("\n- i\r\n- j\n")] - - [TestCase("\r- i\n- j\n")] - [TestCase("\r- i\r- j\n")] - [TestCase("\r- i\r\n- j\n")] - - [TestCase("\r\n- i\n- j\n")] - [TestCase("\r\n- i\r- j\n")] - [TestCase("\r\n- i\r\n- j\n")] - - [TestCase("- i\n- j\r")] - [TestCase("- i\r- j\r")] - [TestCase("- i\r\n- j\r")] - - [TestCase("\n- i\n- j\r")] - [TestCase("\n- i\r- j\r")] - [TestCase("\n- i\r\n- j\r")] - - [TestCase("\r- i\n- j\r")] - [TestCase("\r- i\r- j\r")] - [TestCase("\r- i\r\n- j\r")] - - [TestCase("\r\n- i\n- j\r")] - [TestCase("\r\n- i\r- j\r")] - [TestCase("\r\n- i\r\n- j\r")] - - [TestCase("- i\n- j\r\n")] - [TestCase("- i\r- j\r\n")] - [TestCase("- i\r\n- j\r\n")] - - [TestCase("\n- i\n- j\r\n")] - [TestCase("\n- i\r- j\r\n")] - [TestCase("\n- i\r\n- j\r\n")] - - [TestCase("\r- i\n- j\r\n")] - [TestCase("\r- i\r- j\r\n")] - [TestCase("\r- i\r\n- j\r\n")] - - [TestCase("\r\n- i\n- j\r\n")] - [TestCase("\r\n- i\r- j\r\n")] - [TestCase("\r\n- i\r\n- j\r\n")] - - [TestCase("- i\n")] - [TestCase("- i\n\n")] - [TestCase("- i\n\n\n")] - [TestCase("- i\n\n\n\n")] - public void TestNewline(string value) - { - RoundTrip(value); - } + RoundTrip(value); } } diff --git a/src/Markdig.Tests/RoundtripSpecs/TestYamlFrontMatterBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestYamlFrontMatterBlock.cs index de769c4d8..b7e508968 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestYamlFrontMatterBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestYamlFrontMatterBlock.cs @@ -1,25 +1,15 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Markdig.Renderers.Roundtrip; -using Markdig.Syntax; -using NUnit.Framework; using static Markdig.Tests.TestRoundtrip; -namespace Markdig.Tests.RoundtripSpecs +namespace Markdig.Tests.RoundtripSpecs; + +[TestFixture] +public class TestYamlFrontMatterBlock { - [TestFixture] - public class TestYamlFrontMatterBlock + [TestCase("---\nkey1: value1\nkey2: value2\n---\n\nContent\n")] + [TestCase("No front matter")] + [TestCase("Looks like front matter but actually is not\n---\nkey1: value1\nkey2: value2\n---")] + public void FrontMatterBlockIsPreserved(string value) { - [TestCase("---\nkey1: value1\nkey2: value2\n---\n\nContent\n")] - [TestCase("No front matter")] - [TestCase("Looks like front matter but actually is not\n---\nkey1: value1\nkey2: value2\n---")] - public void FrontMatterBlockIsPreserved(string value) - { - RoundTrip(value); - } + RoundTrip(value); } } diff --git a/src/Markdig.Tests/TestCharHelper.cs b/src/Markdig.Tests/TestCharHelper.cs index 69d8c2a62..ce6099bd8 100644 --- a/src/Markdig.Tests/TestCharHelper.cs +++ b/src/Markdig.Tests/TestCharHelper.cs @@ -1,92 +1,90 @@ -using System.Collections.Generic; using System.Globalization; + using Markdig.Helpers; -using NUnit.Framework; -namespace Markdig.Tests +namespace Markdig.Tests; + +public class TestCharHelper { - public class TestCharHelper + // An ASCII punctuation character is + // !, ", #, $, %, &, ', (, ), *, +, ,, -, ., / (U+0021–2F), + // :, ;, <, =, >, ?, @ (U+003A–0040), + // [, \, ], ^, _, ` (U+005B–0060), + // {, |, }, or ~ (U+007B–007E). + private static readonly HashSet s_asciiPunctuation = new() { - // An ASCII punctuation character is - // !, ", #, $, %, &, ', (, ), *, +, ,, -, ., / (U+0021–2F), - // :, ;, <, =, >, ?, @ (U+003A–0040), - // [, \, ], ^, _, ` (U+005B–0060), - // {, |, }, or ~ (U+007B–007E). - private static readonly HashSet s_asciiPunctuation = new() - { - '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', - ':', ';', '<', '=', '>', '?', '@', - '[', '\\', ']', '^', '_', '`', - '{', '|', '}', '~' - }; + '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', + ':', ';', '<', '=', '>', '?', '@', + '[', '\\', ']', '^', '_', '`', + '{', '|', '}', '~' + }; - // A Unicode punctuation character is an ASCII punctuation character or anything in the general Unicode categories - // Pc, Pd, Pe, Pf, Pi, Po, or Ps. - private static readonly HashSet s_punctuationCategories = new() - { - UnicodeCategory.ConnectorPunctuation, - UnicodeCategory.DashPunctuation, - UnicodeCategory.ClosePunctuation, - UnicodeCategory.FinalQuotePunctuation, - UnicodeCategory.InitialQuotePunctuation, - UnicodeCategory.OtherPunctuation, - UnicodeCategory.OpenPunctuation - }; + // A Unicode punctuation character is an ASCII punctuation character or anything in the general Unicode categories + // Pc, Pd, Pe, Pf, Pi, Po, or Ps. + private static readonly HashSet s_punctuationCategories = new() + { + UnicodeCategory.ConnectorPunctuation, + UnicodeCategory.DashPunctuation, + UnicodeCategory.ClosePunctuation, + UnicodeCategory.FinalQuotePunctuation, + UnicodeCategory.InitialQuotePunctuation, + UnicodeCategory.OtherPunctuation, + UnicodeCategory.OpenPunctuation + }; - private static bool ExpectedIsPunctuation(char c) - { - return c <= 127 - ? s_asciiPunctuation.Contains(c) - : s_punctuationCategories.Contains(CharUnicodeInfo.GetUnicodeCategory(c)); - } + private static bool ExpectedIsPunctuation(char c) + { + return c <= 127 + ? s_asciiPunctuation.Contains(c) + : s_punctuationCategories.Contains(CharUnicodeInfo.GetUnicodeCategory(c)); + } - private static bool ExpectedIsWhitespace(char c) - { - // A Unicode whitespace character is any code point in the Unicode Zs general category, - // or a tab (U+0009), line feed (U+000A), form feed (U+000C), or carriage return (U+000D). - return c == '\t' || c == '\n' || c == '\u000C' || c == '\r' || - CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.SpaceSeparator; - } + private static bool ExpectedIsWhitespace(char c) + { + // A Unicode whitespace character is any code point in the Unicode Zs general category, + // or a tab (U+0009), line feed (U+000A), form feed (U+000C), or carriage return (U+000D). + return c == '\t' || c == '\n' || c == '\u000C' || c == '\r' || + CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.SpaceSeparator; + } - [Test] - public void IsWhitespace() + [Test] + public void IsWhitespace() + { + for (int i = char.MinValue; i <= char.MaxValue; i++) { - for (int i = char.MinValue; i <= char.MaxValue; i++) - { - char c = (char)i; + char c = (char)i; - Assert.AreEqual(ExpectedIsWhitespace(c), CharHelper.IsWhitespace(c)); - } + Assert.AreEqual(ExpectedIsWhitespace(c), CharHelper.IsWhitespace(c)); } + } - [Test] - public void CheckUnicodeCategory() + [Test] + public void CheckUnicodeCategory() + { + for (int i = char.MinValue; i <= char.MaxValue; i++) { - for (int i = char.MinValue; i <= char.MaxValue; i++) - { - char c = (char)i; + char c = (char)i; - bool expectedSpace = c == 0 || ExpectedIsWhitespace(c); - bool expectedPunctuation = c == 0 || ExpectedIsPunctuation(c); + bool expectedSpace = c == 0 || ExpectedIsWhitespace(c); + bool expectedPunctuation = c == 0 || ExpectedIsPunctuation(c); - CharHelper.CheckUnicodeCategory(c, out bool spaceActual, out bool punctuationActual); + CharHelper.CheckUnicodeCategory(c, out bool spaceActual, out bool punctuationActual); - Assert.AreEqual(expectedSpace, spaceActual); - Assert.AreEqual(expectedPunctuation, punctuationActual); - } + Assert.AreEqual(expectedSpace, spaceActual); + Assert.AreEqual(expectedPunctuation, punctuationActual); } + } - [Test] - public void IsSpaceOrPunctuation() + [Test] + public void IsSpaceOrPunctuation() + { + for (int i = char.MinValue; i <= char.MaxValue; i++) { - for (int i = char.MinValue; i <= char.MaxValue; i++) - { - char c = (char)i; + char c = (char)i; - bool expected = c == 0 || ExpectedIsWhitespace(c) || ExpectedIsPunctuation(c); + bool expected = c == 0 || ExpectedIsWhitespace(c) || ExpectedIsPunctuation(c); - Assert.AreEqual(expected, CharHelper.IsSpaceOrPunctuation(c)); - } + Assert.AreEqual(expected, CharHelper.IsSpaceOrPunctuation(c)); } } } diff --git a/src/Markdig.Tests/TestConfigureNewLine.cs b/src/Markdig.Tests/TestConfigureNewLine.cs index a8368ed7a..2429c053b 100644 --- a/src/Markdig.Tests/TestConfigureNewLine.cs +++ b/src/Markdig.Tests/TestConfigureNewLine.cs @@ -1,42 +1,39 @@ -using NUnit.Framework; +namespace Markdig.Tests; -namespace Markdig.Tests +[TestFixture] +public class TestConfigureNewLine { - [TestFixture] - public class TestConfigureNewLine + [Test] + [TestCase(/* newLineForWriting: */ "\n", /* markdownText: */ "*1*\n*2*\n", /* expected: */ "

    1\n2

    \n")] + [TestCase(/* newLineForWriting: */ "\n", /* markdownText: */ "*1*\r\n*2*\r\n", /* expected: */ "

    1\n2

    \n")] + [TestCase(/* newLineForWriting: */ "\r\n", /* markdownText: */ "*1*\n*2*\n", /* expected: */ "

    1\r\n2

    \r\n")] + [TestCase(/* newLineForWriting: */ "\r\n", /* markdownText: */ "*1*\r\n*2*\r\n", /* expected: */ "

    1\r\n2

    \r\n")] + [TestCase(/* newLineForWriting: */ "!!!" , /* markdownText: */ "*1*\n*2*\n", /* expected: */ "

    1!!!2

    !!!")] + [TestCase(/* newLineForWriting: */ "!!!" , /* markdownText: */ "*1*\r\n*2*\r\n", /* expected: */ "

    1!!!2

    !!!")] + public void TestHtmlOutputWhenConfiguringNewLine(string newLineForWriting, string markdownText, string expected) { - [Test] - [TestCase(/* newLineForWriting: */ "\n", /* markdownText: */ "*1*\n*2*\n", /* expected: */ "

    1\n2

    \n")] - [TestCase(/* newLineForWriting: */ "\n", /* markdownText: */ "*1*\r\n*2*\r\n", /* expected: */ "

    1\n2

    \n")] - [TestCase(/* newLineForWriting: */ "\r\n", /* markdownText: */ "*1*\n*2*\n", /* expected: */ "

    1\r\n2

    \r\n")] - [TestCase(/* newLineForWriting: */ "\r\n", /* markdownText: */ "*1*\r\n*2*\r\n", /* expected: */ "

    1\r\n2

    \r\n")] - [TestCase(/* newLineForWriting: */ "!!!" , /* markdownText: */ "*1*\n*2*\n", /* expected: */ "

    1!!!2

    !!!")] - [TestCase(/* newLineForWriting: */ "!!!" , /* markdownText: */ "*1*\r\n*2*\r\n", /* expected: */ "

    1!!!2

    !!!")] - public void TestHtmlOutputWhenConfiguringNewLine(string newLineForWriting, string markdownText, string expected) - { - var pipeline = new MarkdownPipelineBuilder() - .ConfigureNewLine(newLineForWriting) - .Build(); + var pipeline = new MarkdownPipelineBuilder() + .ConfigureNewLine(newLineForWriting) + .Build(); - var actual = Markdown.ToHtml(markdownText, pipeline); - Assert.AreEqual(expected, actual); - } + var actual = Markdown.ToHtml(markdownText, pipeline); + Assert.AreEqual(expected, actual); + } - [Test] - [TestCase(/* newLineForWriting: */ "\n", /* markdownText: */ "*1*\n*2*\n", /* expected: */ "1\n2\n")] - [TestCase(/* newLineForWriting: */ "\n", /* markdownText: */ "*1*\r\n*2*\r\n", /* expected: */ "1\n2\n")] - [TestCase(/* newLineForWriting: */ "\r\n", /* markdownText: */ "*1*\n*2*\n", /* expected: */ "1\r\n2\r\n")] - [TestCase(/* newLineForWriting: */ "\r\n", /* markdownText: */ "*1*\r\n*2*\r\n", /* expected: */ "1\r\n2\r\n")] - [TestCase(/* newLineForWriting: */ "!!!", /* markdownText: */ "*1*\n*2*\n", /* expected: */ "1!!!2!!!")] - [TestCase(/* newLineForWriting: */ "!!!", /* markdownText: */ "*1*\r\n*2*\r\n", /* expected: */ "1!!!2!!!")] - public void TestPlainOutputWhenConfiguringNewLine(string newLineForWriting, string markdownText, string expected) - { - var pipeline = new MarkdownPipelineBuilder() - .ConfigureNewLine(newLineForWriting) - .Build(); + [Test] + [TestCase(/* newLineForWriting: */ "\n", /* markdownText: */ "*1*\n*2*\n", /* expected: */ "1\n2\n")] + [TestCase(/* newLineForWriting: */ "\n", /* markdownText: */ "*1*\r\n*2*\r\n", /* expected: */ "1\n2\n")] + [TestCase(/* newLineForWriting: */ "\r\n", /* markdownText: */ "*1*\n*2*\n", /* expected: */ "1\r\n2\r\n")] + [TestCase(/* newLineForWriting: */ "\r\n", /* markdownText: */ "*1*\r\n*2*\r\n", /* expected: */ "1\r\n2\r\n")] + [TestCase(/* newLineForWriting: */ "!!!", /* markdownText: */ "*1*\n*2*\n", /* expected: */ "1!!!2!!!")] + [TestCase(/* newLineForWriting: */ "!!!", /* markdownText: */ "*1*\r\n*2*\r\n", /* expected: */ "1!!!2!!!")] + public void TestPlainOutputWhenConfiguringNewLine(string newLineForWriting, string markdownText, string expected) + { + var pipeline = new MarkdownPipelineBuilder() + .ConfigureNewLine(newLineForWriting) + .Build(); - var actual = Markdown.ToPlainText(markdownText, pipeline); - Assert.AreEqual(expected, actual); - } + var actual = Markdown.ToPlainText(markdownText, pipeline); + Assert.AreEqual(expected, actual); } } \ No newline at end of file diff --git a/src/Markdig.Tests/TestContainerBlocks.cs b/src/Markdig.Tests/TestContainerBlocks.cs index a7405189d..abffe84fe 100644 --- a/src/Markdig.Tests/TestContainerBlocks.cs +++ b/src/Markdig.Tests/TestContainerBlocks.cs @@ -1,189 +1,186 @@ -using System; using Markdig.Syntax; -using NUnit.Framework; -namespace Markdig.Tests +namespace Markdig.Tests; + +public class TestContainerBlocks { - public class TestContainerBlocks + private class MockContainerBlock : ContainerBlock { - private class MockContainerBlock : ContainerBlock + public MockContainerBlock() + : base(null) { - public MockContainerBlock() - : base(null) - { - } } + } - [Test] - public void CanBeCleared() - { - ContainerBlock container = new MockContainerBlock(); - Assert.AreEqual(0, container.Count); - Assert.Null(container.LastChild); + [Test] + public void CanBeCleared() + { + ContainerBlock container = new MockContainerBlock(); + Assert.AreEqual(0, container.Count); + Assert.Null(container.LastChild); - var paragraph = new ParagraphBlock(); - Assert.Null(paragraph.Parent); + var paragraph = new ParagraphBlock(); + Assert.Null(paragraph.Parent); - container.Add(paragraph); + container.Add(paragraph); - Assert.AreEqual(1, container.Count); - Assert.AreSame(container, paragraph.Parent); - Assert.AreSame(paragraph, container.LastChild); + Assert.AreEqual(1, container.Count); + Assert.AreSame(container, paragraph.Parent); + Assert.AreSame(paragraph, container.LastChild); - container.Clear(); + container.Clear(); - Assert.AreEqual(0, container.Count); - Assert.Null(container.LastChild); - Assert.Null(paragraph.Parent); - } + Assert.AreEqual(0, container.Count); + Assert.Null(container.LastChild); + Assert.Null(paragraph.Parent); + } - [Test] - public void CanBeInsertedInto() - { - ContainerBlock container = new MockContainerBlock(); - - var one = new ParagraphBlock(); - container.Insert(0, one); - Assert.AreEqual(1, container.Count); - Assert.AreSame(container[0], one); - Assert.AreSame(container, one.Parent); - - var two = new ParagraphBlock(); - container.Insert(1, two); - Assert.AreEqual(2, container.Count); - Assert.AreSame(container[0], one); - Assert.AreSame(container[1], two); - Assert.AreSame(container, two.Parent); - - var three = new ParagraphBlock(); - container.Insert(0, three); - Assert.AreEqual(3, container.Count); - Assert.AreSame(container[0], three); - Assert.AreSame(container[1], one); - Assert.AreSame(container[2], two); - Assert.AreSame(container, three.Parent); - - Assert.Throws(() => container.Insert(0, null)); - Assert.Throws(() => container.Insert(4, new ParagraphBlock())); - Assert.Throws(() => container.Insert(-1, new ParagraphBlock())); - Assert.Throws(() => container.Insert(0, one)); // one already has a parent - } + [Test] + public void CanBeInsertedInto() + { + ContainerBlock container = new MockContainerBlock(); + + var one = new ParagraphBlock(); + container.Insert(0, one); + Assert.AreEqual(1, container.Count); + Assert.AreSame(container[0], one); + Assert.AreSame(container, one.Parent); + + var two = new ParagraphBlock(); + container.Insert(1, two); + Assert.AreEqual(2, container.Count); + Assert.AreSame(container[0], one); + Assert.AreSame(container[1], two); + Assert.AreSame(container, two.Parent); + + var three = new ParagraphBlock(); + container.Insert(0, three); + Assert.AreEqual(3, container.Count); + Assert.AreSame(container[0], three); + Assert.AreSame(container[1], one); + Assert.AreSame(container[2], two); + Assert.AreSame(container, three.Parent); + + Assert.Throws(() => container.Insert(0, null)); + Assert.Throws(() => container.Insert(4, new ParagraphBlock())); + Assert.Throws(() => container.Insert(-1, new ParagraphBlock())); + Assert.Throws(() => container.Insert(0, one)); // one already has a parent + } - [Test] - public void CanBeSet() - { - ContainerBlock container = new MockContainerBlock(); + [Test] + public void CanBeSet() + { + ContainerBlock container = new MockContainerBlock(); - var one = new ParagraphBlock(); - container.Insert(0, one); - Assert.AreEqual(1, container.Count); - Assert.AreSame(container[0], one); - Assert.AreSame(container, one.Parent); + var one = new ParagraphBlock(); + container.Insert(0, one); + Assert.AreEqual(1, container.Count); + Assert.AreSame(container[0], one); + Assert.AreSame(container, one.Parent); - var two = new ParagraphBlock(); - container[0] = two; - Assert.AreSame(container, two.Parent); - Assert.Null(one.Parent); + var two = new ParagraphBlock(); + container[0] = two; + Assert.AreSame(container, two.Parent); + Assert.Null(one.Parent); - Assert.Throws(() => container[0] = two); // two already has a parent - } + Assert.Throws(() => container[0] = two); // two already has a parent + } - [Test] - public void Contains() - { - var container = new MockContainerBlock(); - var block = new ParagraphBlock(); + [Test] + public void Contains() + { + var container = new MockContainerBlock(); + var block = new ParagraphBlock(); - Assert.False(container.Contains(block)); + Assert.False(container.Contains(block)); - container.Add(block); - Assert.True(container.Contains(block)); + container.Add(block); + Assert.True(container.Contains(block)); - container.Add(new ParagraphBlock()); - Assert.True(container.Contains(block)); + container.Add(new ParagraphBlock()); + Assert.True(container.Contains(block)); - container.Insert(0, new ParagraphBlock()); - Assert.True(container.Contains(block)); - } + container.Insert(0, new ParagraphBlock()); + Assert.True(container.Contains(block)); + } - [Test] - public void Remove() - { - var container = new MockContainerBlock(); - var block = new ParagraphBlock(); - - Assert.False(container.Remove(block)); - Assert.AreEqual(0, container.Count); - Assert.Throws(() => container.RemoveAt(0)); - Assert.AreEqual(0, container.Count); - - container.Add(block); - Assert.AreEqual(1, container.Count); - Assert.True(container.Remove(block)); - Assert.AreEqual(0, container.Count); - Assert.False(container.Remove(block)); - Assert.AreEqual(0, container.Count); - - container.Add(block); - Assert.AreEqual(1, container.Count); - container.RemoveAt(0); - Assert.AreEqual(0, container.Count); - Assert.Throws(() => container.RemoveAt(0)); - Assert.AreEqual(0, container.Count); - - container.Add(new ParagraphBlock { Column = 1 }); - container.Add(new ParagraphBlock { Column = 2 }); - container.Add(new ParagraphBlock { Column = 3 }); - container.Add(new ParagraphBlock { Column = 4 }); - Assert.AreEqual(4, container.Count); - - container.RemoveAt(2); - Assert.AreEqual(3, container.Count); - Assert.AreEqual(4, container[2].Column); - - Assert.True(container.Remove(container[1])); - Assert.AreEqual(2, container.Count); - Assert.AreEqual(1, container[0].Column); - Assert.AreEqual(4, container[1].Column); - Assert.Throws(() => _ = container[2]); - } + [Test] + public void Remove() + { + var container = new MockContainerBlock(); + var block = new ParagraphBlock(); + + Assert.False(container.Remove(block)); + Assert.AreEqual(0, container.Count); + Assert.Throws(() => container.RemoveAt(0)); + Assert.AreEqual(0, container.Count); + + container.Add(block); + Assert.AreEqual(1, container.Count); + Assert.True(container.Remove(block)); + Assert.AreEqual(0, container.Count); + Assert.False(container.Remove(block)); + Assert.AreEqual(0, container.Count); + + container.Add(block); + Assert.AreEqual(1, container.Count); + container.RemoveAt(0); + Assert.AreEqual(0, container.Count); + Assert.Throws(() => container.RemoveAt(0)); + Assert.AreEqual(0, container.Count); + + container.Add(new ParagraphBlock { Column = 1 }); + container.Add(new ParagraphBlock { Column = 2 }); + container.Add(new ParagraphBlock { Column = 3 }); + container.Add(new ParagraphBlock { Column = 4 }); + Assert.AreEqual(4, container.Count); + + container.RemoveAt(2); + Assert.AreEqual(3, container.Count); + Assert.AreEqual(4, container[2].Column); + + Assert.True(container.Remove(container[1])); + Assert.AreEqual(2, container.Count); + Assert.AreEqual(1, container[0].Column); + Assert.AreEqual(4, container[1].Column); + Assert.Throws(() => _ = container[2]); + } - [Test] - public void CopyTo() - { - var container = new MockContainerBlock(); - - var destination = new Block[4]; - container.CopyTo(destination, 0); - container.CopyTo(destination, 1); - container.CopyTo(destination, -1); - container.CopyTo(destination, 5); - Assert.Null(destination[0]); - - container.Add(new ParagraphBlock()); - container.CopyTo(destination, 0); - Assert.NotNull(destination[0]); - Assert.Null(destination[1]); - Assert.Null(destination[2]); - Assert.Null(destination[3]); - - container.CopyTo(destination, 2); - Assert.NotNull(destination[0]); - Assert.Null(destination[1]); - Assert.NotNull(destination[2]); - Assert.Null(destination[3]); - - Array.Clear(destination); - - container.Add(new ParagraphBlock()); - container.CopyTo(destination, 1); - Assert.Null(destination[0]); - Assert.NotNull(destination[1]); - Assert.NotNull(destination[2]); - Assert.Null(destination[3]); - - Assert.Throws(() => container.CopyTo(destination, 3)); - } + [Test] + public void CopyTo() + { + var container = new MockContainerBlock(); + + var destination = new Block[4]; + container.CopyTo(destination, 0); + container.CopyTo(destination, 1); + container.CopyTo(destination, -1); + container.CopyTo(destination, 5); + Assert.Null(destination[0]); + + container.Add(new ParagraphBlock()); + container.CopyTo(destination, 0); + Assert.NotNull(destination[0]); + Assert.Null(destination[1]); + Assert.Null(destination[2]); + Assert.Null(destination[3]); + + container.CopyTo(destination, 2); + Assert.NotNull(destination[0]); + Assert.Null(destination[1]); + Assert.NotNull(destination[2]); + Assert.Null(destination[3]); + + Array.Clear(destination); + + container.Add(new ParagraphBlock()); + container.CopyTo(destination, 1); + Assert.Null(destination[0]); + Assert.NotNull(destination[1]); + Assert.NotNull(destination[2]); + Assert.Null(destination[3]); + + Assert.Throws(() => container.CopyTo(destination, 3)); } } diff --git a/src/Markdig.Tests/TestContainerInlines.cs b/src/Markdig.Tests/TestContainerInlines.cs index ef79e00dc..90520f1ec 100644 --- a/src/Markdig.Tests/TestContainerInlines.cs +++ b/src/Markdig.Tests/TestContainerInlines.cs @@ -1,41 +1,38 @@ -using System; using Markdig.Syntax; using Markdig.Syntax.Inlines; -using NUnit.Framework; -namespace Markdig.Tests +namespace Markdig.Tests; + +public class TestContainerInlines { - public class TestContainerInlines + private class MockLeafBlock : LeafBlock { - private class MockLeafBlock : LeafBlock + public MockLeafBlock() + : base(null) { - public MockLeafBlock() - : base(null) - { - } } + } - [Test] - public void CanBeAddedToLeafBlock() - { - var leafBlock1 = new MockLeafBlock(); + [Test] + public void CanBeAddedToLeafBlock() + { + var leafBlock1 = new MockLeafBlock(); - var one = new ContainerInline(); - Assert.Null(one.ParentBlock); + var one = new ContainerInline(); + Assert.Null(one.ParentBlock); - leafBlock1.Inline = one; - Assert.AreSame(leafBlock1, one.ParentBlock); + leafBlock1.Inline = one; + Assert.AreSame(leafBlock1, one.ParentBlock); - var two = new ContainerInline(); - Assert.Null(two.ParentBlock); + var two = new ContainerInline(); + Assert.Null(two.ParentBlock); - leafBlock1.Inline = two; - Assert.AreSame(leafBlock1, two.ParentBlock); - Assert.Null(one.ParentBlock); + leafBlock1.Inline = two; + Assert.AreSame(leafBlock1, two.ParentBlock); + Assert.Null(one.ParentBlock); - var leafBlock2 = new MockLeafBlock(); - Assert.Throws(() => leafBlock2.Inline = two); - } + var leafBlock2 = new MockLeafBlock(); + Assert.Throws(() => leafBlock2.Inline = two); } } diff --git a/src/Markdig.Tests/TestCustomEmojis.cs b/src/Markdig.Tests/TestCustomEmojis.cs index 273b332e1..c8f2432a7 100644 --- a/src/Markdig.Tests/TestCustomEmojis.cs +++ b/src/Markdig.Tests/TestCustomEmojis.cs @@ -1,128 +1,124 @@ -using System; -using System.Collections.Generic; using Markdig.Extensions.Emoji; -using NUnit.Framework; -namespace Markdig.Tests +namespace Markdig.Tests; + +[TestFixture] +public class TestCustomEmojis { - [TestFixture] - public class TestCustomEmojis + [Test] + [TestCase(":smiley:", "

    \n")] + [TestCase(":confused:", "

    :confused:

    \n")] // default emoji does not work + [TestCase(":/", "

    :/

    \n")] // default smiley does not work + public void TestCustomEmoji(string input, string expected) + { + var emojiToUnicode = new Dictionary(); + var smileyToEmoji = new Dictionary(); + + emojiToUnicode[":smiley:"] = "♥"; + + var customMapping = new EmojiMapping(emojiToUnicode, smileyToEmoji); + + var pipeline = new MarkdownPipelineBuilder() + .UseEmojiAndSmiley(customEmojiMapping: customMapping) + .Build(); + + var actual = Markdown.ToHtml(input, pipeline); + Assert.AreEqual(expected, actual); + } + + [Test] + [TestCase(":testheart:", "

    \n")] + [TestCase("hello", "

    \n")] + [TestCase(":confused:", "

    :confused:

    \n")] // default emoji does not work + [TestCase(":/", "

    :/

    \n")] // default smiley does not work + public void TestCustomSmiley(string input, string expected) + { + var emojiToUnicode = new Dictionary(); + var smileyToEmoji = new Dictionary(); + + emojiToUnicode[":testheart:"] = "♥"; + smileyToEmoji["hello"] = ":testheart:"; + + var customMapping = new EmojiMapping(emojiToUnicode, smileyToEmoji); + + var pipeline = new MarkdownPipelineBuilder() + .UseEmojiAndSmiley(customEmojiMapping: customMapping) + .Build(); + + var actual = Markdown.ToHtml(input, pipeline); + Assert.AreEqual(expected, actual); + } + + [Test] + [TestCase(":smiley:", "

    \n")] + [TestCase(":)", "

    \n")] + [TestCase(":confused:", "

    😕

    \n")] // default emoji still works + [TestCase(":/", "

    😕

    \n")] // default smiley still works + public void TestOverrideDefaultWithCustomEmoji(string input, string expected) + { + var emojiToUnicode = EmojiMapping.GetDefaultEmojiShortcodeToUnicode(); + var smileyToEmoji = EmojiMapping.GetDefaultSmileyToEmojiShortcode(); + + emojiToUnicode[":smiley:"] = "♥"; + + var customMapping = new EmojiMapping(emojiToUnicode, smileyToEmoji); + + var pipeline = new MarkdownPipelineBuilder() + .UseEmojiAndSmiley(customEmojiMapping: customMapping) + .Build(); + + var actual = Markdown.ToHtml(input, pipeline); + Assert.AreEqual(expected, actual); + } + + [Test] + [TestCase(":testheart:", "

    \n")] + [TestCase("hello", "

    \n")] + [TestCase(":confused:", "

    😕

    \n")] // default emoji still works + [TestCase(":/", "

    😕

    \n")] // default smiley still works + public void TestOverrideDefaultWithCustomSmiley(string input, string expected) { - [Test] - [TestCase(":smiley:", "

    \n")] - [TestCase(":confused:", "

    :confused:

    \n")] // default emoji does not work - [TestCase(":/", "

    :/

    \n")] // default smiley does not work - public void TestCustomEmoji(string input, string expected) - { - var emojiToUnicode = new Dictionary(); - var smileyToEmoji = new Dictionary(); - - emojiToUnicode[":smiley:"] = "♥"; - - var customMapping = new EmojiMapping(emojiToUnicode, smileyToEmoji); - - var pipeline = new MarkdownPipelineBuilder() - .UseEmojiAndSmiley(customEmojiMapping: customMapping) - .Build(); - - var actual = Markdown.ToHtml(input, pipeline); - Assert.AreEqual(expected, actual); - } - - [Test] - [TestCase(":testheart:", "

    \n")] - [TestCase("hello", "

    \n")] - [TestCase(":confused:", "

    :confused:

    \n")] // default emoji does not work - [TestCase(":/", "

    :/

    \n")] // default smiley does not work - public void TestCustomSmiley(string input, string expected) - { - var emojiToUnicode = new Dictionary(); - var smileyToEmoji = new Dictionary(); - - emojiToUnicode[":testheart:"] = "♥"; - smileyToEmoji["hello"] = ":testheart:"; - - var customMapping = new EmojiMapping(emojiToUnicode, smileyToEmoji); - - var pipeline = new MarkdownPipelineBuilder() - .UseEmojiAndSmiley(customEmojiMapping: customMapping) - .Build(); - - var actual = Markdown.ToHtml(input, pipeline); - Assert.AreEqual(expected, actual); - } - - [Test] - [TestCase(":smiley:", "

    \n")] - [TestCase(":)", "

    \n")] - [TestCase(":confused:", "

    😕

    \n")] // default emoji still works - [TestCase(":/", "

    😕

    \n")] // default smiley still works - public void TestOverrideDefaultWithCustomEmoji(string input, string expected) - { - var emojiToUnicode = EmojiMapping.GetDefaultEmojiShortcodeToUnicode(); - var smileyToEmoji = EmojiMapping.GetDefaultSmileyToEmojiShortcode(); - - emojiToUnicode[":smiley:"] = "♥"; - - var customMapping = new EmojiMapping(emojiToUnicode, smileyToEmoji); - - var pipeline = new MarkdownPipelineBuilder() - .UseEmojiAndSmiley(customEmojiMapping: customMapping) - .Build(); - - var actual = Markdown.ToHtml(input, pipeline); - Assert.AreEqual(expected, actual); - } - - [Test] - [TestCase(":testheart:", "

    \n")] - [TestCase("hello", "

    \n")] - [TestCase(":confused:", "

    😕

    \n")] // default emoji still works - [TestCase(":/", "

    😕

    \n")] // default smiley still works - public void TestOverrideDefaultWithCustomSmiley(string input, string expected) - { - var emojiToUnicode = EmojiMapping.GetDefaultEmojiShortcodeToUnicode(); - var smileyToEmoji = EmojiMapping.GetDefaultSmileyToEmojiShortcode(); - - emojiToUnicode[":testheart:"] = "♥"; - smileyToEmoji["hello"] = ":testheart:"; - - var customMapping = new EmojiMapping(emojiToUnicode, smileyToEmoji); - - var pipeline = new MarkdownPipelineBuilder() - .UseEmojiAndSmiley(customEmojiMapping: customMapping) - .Build(); - - var actual = Markdown.ToHtml(input, pipeline); - Assert.AreEqual(expected, actual); - } - - [Test] - public void TestCustomEmojiValidation() - { - var emojiToUnicode = new Dictionary(); - var smileyToEmoji = new Dictionary(); - - Assert.Throws(() => new EmojiMapping(null, smileyToEmoji)); - Assert.Throws(() => new EmojiMapping(emojiToUnicode, null)); - - emojiToUnicode.Add("null-value", null); - Assert.Throws(() => new EmojiMapping(emojiToUnicode, smileyToEmoji)); - emojiToUnicode.Clear(); - - smileyToEmoji.Add("null-value", null); - Assert.Throws(() => new EmojiMapping(emojiToUnicode, smileyToEmoji)); - smileyToEmoji.Clear(); - - smileyToEmoji.Add("foo", "something-that-does-not-exist-in-emojiToUnicode"); - Assert.Throws(() => new EmojiMapping(emojiToUnicode, smileyToEmoji)); - smileyToEmoji.Clear(); - - emojiToUnicode.Add("a", "aaa"); - emojiToUnicode.Add("b", "bbb"); - emojiToUnicode.Add("c", "ccc"); - smileyToEmoji.Add("a", "c"); // "a" already exists in emojiToUnicode - Assert.Throws(() => new EmojiMapping(emojiToUnicode, smileyToEmoji)); - } + var emojiToUnicode = EmojiMapping.GetDefaultEmojiShortcodeToUnicode(); + var smileyToEmoji = EmojiMapping.GetDefaultSmileyToEmojiShortcode(); + + emojiToUnicode[":testheart:"] = "♥"; + smileyToEmoji["hello"] = ":testheart:"; + + var customMapping = new EmojiMapping(emojiToUnicode, smileyToEmoji); + + var pipeline = new MarkdownPipelineBuilder() + .UseEmojiAndSmiley(customEmojiMapping: customMapping) + .Build(); + + var actual = Markdown.ToHtml(input, pipeline); + Assert.AreEqual(expected, actual); + } + + [Test] + public void TestCustomEmojiValidation() + { + var emojiToUnicode = new Dictionary(); + var smileyToEmoji = new Dictionary(); + + Assert.Throws(() => new EmojiMapping(null, smileyToEmoji)); + Assert.Throws(() => new EmojiMapping(emojiToUnicode, null)); + + emojiToUnicode.Add("null-value", null); + Assert.Throws(() => new EmojiMapping(emojiToUnicode, smileyToEmoji)); + emojiToUnicode.Clear(); + + smileyToEmoji.Add("null-value", null); + Assert.Throws(() => new EmojiMapping(emojiToUnicode, smileyToEmoji)); + smileyToEmoji.Clear(); + + smileyToEmoji.Add("foo", "something-that-does-not-exist-in-emojiToUnicode"); + Assert.Throws(() => new EmojiMapping(emojiToUnicode, smileyToEmoji)); + smileyToEmoji.Clear(); + + emojiToUnicode.Add("a", "aaa"); + emojiToUnicode.Add("b", "bbb"); + emojiToUnicode.Add("c", "ccc"); + smileyToEmoji.Add("a", "c"); // "a" already exists in emojiToUnicode + Assert.Throws(() => new EmojiMapping(emojiToUnicode, smileyToEmoji)); } } \ No newline at end of file diff --git a/src/Markdig.Tests/TestDescendantsOrder.cs b/src/Markdig.Tests/TestDescendantsOrder.cs index adf9e3171..82ed8293e 100644 --- a/src/Markdig.Tests/TestDescendantsOrder.cs +++ b/src/Markdig.Tests/TestDescendantsOrder.cs @@ -1,151 +1,145 @@ -using NUnit.Framework; -using Markdig.Helpers; using Markdig.Syntax; using Markdig.Syntax.Inlines; -using System; -using System.Linq; -using System.Collections.Generic; -namespace Markdig.Tests +namespace Markdig.Tests; + +[TestFixture] +public class TestDescendantsOrder { - [TestFixture] - public class TestDescendantsOrder + public static void TestSchemas(MarkdownDocument[] specsSyntaxTrees) { - public static void TestSchemas(MarkdownDocument[] specsSyntaxTrees) + foreach (var syntaxTree in specsSyntaxTrees) { - foreach (var syntaxTree in specsSyntaxTrees) - { - AssertIEnumerablesAreEqual( - Descendants_Legacy(syntaxTree), - syntaxTree.Descendants()); + AssertIEnumerablesAreEqual( + Descendants_Legacy(syntaxTree), + syntaxTree.Descendants()); - AssertIEnumerablesAreEqual( - syntaxTree.Descendants().OfType(), - syntaxTree.Descendants()); + AssertIEnumerablesAreEqual( + syntaxTree.Descendants().OfType(), + syntaxTree.Descendants()); + AssertIEnumerablesAreEqual( + syntaxTree.Descendants().OfType(), + (syntaxTree as ContainerBlock).Descendants()); + + AssertIEnumerablesAreEqual( + syntaxTree.Descendants().OfType(), + syntaxTree.Descendants()); + + foreach (LiteralInline literalInline in syntaxTree.Descendants()) + { + Assert.AreSame(Array.Empty(), literalInline.Descendants()); + Assert.AreSame(Array.Empty(), literalInline.Descendants()); + Assert.AreSame(Array.Empty(), literalInline.Descendants()); + } + + foreach (ContainerInline containerInline in syntaxTree.Descendants()) + { AssertIEnumerablesAreEqual( - syntaxTree.Descendants().OfType(), - (syntaxTree as ContainerBlock).Descendants()); + containerInline.FindDescendants(), + containerInline.Descendants()); AssertIEnumerablesAreEqual( - syntaxTree.Descendants().OfType(), - syntaxTree.Descendants()); + containerInline.FindDescendants(), + (containerInline as MarkdownObject).Descendants()); - foreach (LiteralInline literalInline in syntaxTree.Descendants()) + if (containerInline.FirstChild is null) { - Assert.AreSame(Array.Empty(), literalInline.Descendants()); - Assert.AreSame(Array.Empty(), literalInline.Descendants()); - Assert.AreSame(Array.Empty(), literalInline.Descendants()); + Assert.AreSame(Array.Empty(), containerInline.Descendants()); + Assert.AreSame(Array.Empty(), containerInline.FindDescendants()); + Assert.AreSame(Array.Empty(), (containerInline as MarkdownObject).Descendants()); } - foreach (ContainerInline containerInline in syntaxTree.Descendants()) - { - AssertIEnumerablesAreEqual( - containerInline.FindDescendants(), - containerInline.Descendants()); - - AssertIEnumerablesAreEqual( - containerInline.FindDescendants(), - (containerInline as MarkdownObject).Descendants()); + Assert.AreSame(Array.Empty(), containerInline.Descendants()); + Assert.AreSame(Array.Empty(), containerInline.Descendants()); + } - if (containerInline.FirstChild is null) - { - Assert.AreSame(Array.Empty(), containerInline.Descendants()); - Assert.AreSame(Array.Empty(), containerInline.FindDescendants()); - Assert.AreSame(Array.Empty(), (containerInline as MarkdownObject).Descendants()); - } + foreach (ParagraphBlock paragraphBlock in syntaxTree.Descendants()) + { + AssertIEnumerablesAreEqual( + (paragraphBlock as MarkdownObject).Descendants(), + paragraphBlock.Descendants()); - Assert.AreSame(Array.Empty(), containerInline.Descendants()); - Assert.AreSame(Array.Empty(), containerInline.Descendants()); - } + Assert.AreSame(Array.Empty(), paragraphBlock.Descendants()); + } - foreach (ParagraphBlock paragraphBlock in syntaxTree.Descendants()) - { - AssertIEnumerablesAreEqual( - (paragraphBlock as MarkdownObject).Descendants(), - paragraphBlock.Descendants()); + foreach (ContainerBlock containerBlock in syntaxTree.Descendants()) + { + AssertIEnumerablesAreEqual( + containerBlock.Descendants(), + (containerBlock as MarkdownObject).Descendants()); - Assert.AreSame(Array.Empty(), paragraphBlock.Descendants()); - } + AssertIEnumerablesAreEqual( + containerBlock.Descendants(), + (containerBlock as MarkdownObject).Descendants()); - foreach (ContainerBlock containerBlock in syntaxTree.Descendants()) + if (containerBlock.Count == 0) { - AssertIEnumerablesAreEqual( - containerBlock.Descendants(), - (containerBlock as MarkdownObject).Descendants()); - - AssertIEnumerablesAreEqual( - containerBlock.Descendants(), - (containerBlock as MarkdownObject).Descendants()); - - if (containerBlock.Count == 0) - { - Assert.AreSame(Array.Empty(), containerBlock.Descendants()); - Assert.AreSame(Array.Empty(), (containerBlock as Block).Descendants()); - Assert.AreSame(Array.Empty(), (containerBlock as MarkdownObject).Descendants()); - } + Assert.AreSame(Array.Empty(), containerBlock.Descendants()); + Assert.AreSame(Array.Empty(), (containerBlock as Block).Descendants()); + Assert.AreSame(Array.Empty(), (containerBlock as MarkdownObject).Descendants()); } } } + } - private static void AssertIEnumerablesAreEqual(IEnumerable first, IEnumerable second) - { - var firstList = new List(first); - var secondList = new List(second); + private static void AssertIEnumerablesAreEqual(IEnumerable first, IEnumerable second) + { + var firstList = new List(first); + var secondList = new List(second); - Assert.AreEqual(firstList.Count, secondList.Count); + Assert.AreEqual(firstList.Count, secondList.Count); - for (int i = 0; i < firstList.Count; i++) - { - Assert.AreSame(firstList[i], secondList[i]); - } + for (int i = 0; i < firstList.Count; i++) + { + Assert.AreSame(firstList[i], secondList[i]); } + } - private static IEnumerable Descendants_Legacy(MarkdownObject markdownObject) - { - // TODO: implement a recursiveless method + private static IEnumerable Descendants_Legacy(MarkdownObject markdownObject) + { + // TODO: implement a recursiveless method - var block = markdownObject as ContainerBlock; - if (block != null) + var block = markdownObject as ContainerBlock; + if (block != null) + { + foreach (var subBlock in block) { - foreach (var subBlock in block) - { - yield return subBlock; + yield return subBlock; - foreach (var sub in Descendants_Legacy(subBlock)) - { - yield return sub; - } + foreach (var sub in Descendants_Legacy(subBlock)) + { + yield return sub; + } - // Visit leaf block that have inlines - var leafBlock = subBlock as LeafBlock; - if (leafBlock?.Inline != null) + // Visit leaf block that have inlines + var leafBlock = subBlock as LeafBlock; + if (leafBlock?.Inline != null) + { + foreach (var subInline in Descendants_Legacy(leafBlock.Inline)) { - foreach (var subInline in Descendants_Legacy(leafBlock.Inline)) - { - yield return subInline; - } + yield return subInline; } } } - else + } + else + { + var inline = markdownObject as ContainerInline; + if (inline != null) { - var inline = markdownObject as ContainerInline; - if (inline != null) + var child = inline.FirstChild; + while (child != null) { - var child = inline.FirstChild; - while (child != null) - { - var next = child.NextSibling; - yield return child; - - foreach (var sub in Descendants_Legacy(child)) - { - yield return sub; - } + var next = child.NextSibling; + yield return child; - child = next; + foreach (var sub in Descendants_Legacy(child)) + { + yield return sub; } + + child = next; } } } diff --git a/src/Markdig.Tests/TestEmphasisExtended.cs b/src/Markdig.Tests/TestEmphasisExtended.cs index 3703280c1..ce6c89737 100644 --- a/src/Markdig.Tests/TestEmphasisExtended.cs +++ b/src/Markdig.Tests/TestEmphasisExtended.cs @@ -1,169 +1,166 @@ +using System.Diagnostics; + using Markdig.Parsers.Inlines; using Markdig.Renderers; using Markdig.Renderers.Html; using Markdig.Syntax.Inlines; -using NUnit.Framework; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -namespace Markdig.Tests +namespace Markdig.Tests; + +[TestFixture] +public class TestEmphasisExtended { - [TestFixture] - public class TestEmphasisExtended + class EmphasisTestExtension : IMarkdownExtension { - class EmphasisTestExtension : IMarkdownExtension + public void Setup(MarkdownPipelineBuilder pipeline) { - public void Setup(MarkdownPipelineBuilder pipeline) - { - var emphasisParser = pipeline.InlineParsers.Find(); - Debug.Assert(emphasisParser != null); + var emphasisParser = pipeline.InlineParsers.Find(); + Debug.Assert(emphasisParser != null); - foreach (var emphasis in EmphasisTestDescriptors) - { - emphasisParser.EmphasisDescriptors.Add( - new EmphasisDescriptor(emphasis.Character, emphasis.Minimum, emphasis.Maximum, true)); - } - emphasisParser.TryCreateEmphasisInlineList.Add((delimiterChar, delimiterCount) => - { - return delimiterChar == '*' || delimiterChar == '_' - ? null - : new CustomEmphasisInline() { DelimiterChar = delimiterChar, DelimiterCount = delimiterCount }; - }); - } - - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + foreach (var emphasis in EmphasisTestDescriptors) { - renderer.ObjectRenderers.Insert(0, new EmphasisRenderer()); + emphasisParser.EmphasisDescriptors.Add( + new EmphasisDescriptor(emphasis.Character, emphasis.Minimum, emphasis.Maximum, true)); } + emphasisParser.TryCreateEmphasisInlineList.Add((delimiterChar, delimiterCount) => + { + return delimiterChar is '*' or '_' + ? null + : new CustomEmphasisInline() { DelimiterChar = delimiterChar, DelimiterCount = delimiterCount }; + }); + } - class EmphasisRenderer : HtmlObjectRenderer + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + renderer.ObjectRenderers.Insert(0, new EmphasisRenderer()); + } + + class EmphasisRenderer : HtmlObjectRenderer + { + protected override void Write(HtmlRenderer renderer, CustomEmphasisInline obj) { - protected override void Write(HtmlRenderer renderer, CustomEmphasisInline obj) - { - var tag = EmphasisTestDescriptors.First(test => test.Character == obj.DelimiterChar).Tags[obj.DelimiterCount]; + var tag = EmphasisTestDescriptors.First(test => test.Character == obj.DelimiterChar).Tags[obj.DelimiterCount]; - renderer.Write(tag.OpeningTag); - renderer.WriteChildren(obj); - renderer.Write(tag.ClosingTag); - } + renderer.Write(tag.OpeningTag); + renderer.WriteChildren(obj); + renderer.Write(tag.ClosingTag); } } - class Tag - { + } + class Tag + { #pragma warning disable CS0649 - public int Level; + public int Level; #pragma warning restore CS0649 - public string RawTag; - public string OpeningTag; - public string ClosingTag; + public string RawTag; + public string OpeningTag; + public string ClosingTag; - public Tag(string tag) - { - RawTag = tag; - OpeningTag = "<" + tag + ">"; - ClosingTag = ""; - } + public Tag(string tag) + { + RawTag = tag; + OpeningTag = "<" + tag + ">"; + ClosingTag = ""; + } + + public static implicit operator Tag(string tag) + => new Tag(tag); + } + class EmphasisTestDescriptor + { + public char Character; + public int Minimum; + public int Maximum; + public Dictionary Tags = new Dictionary(); - public static implicit operator Tag(string tag) - => new Tag(tag); + private EmphasisTestDescriptor(char character, int min, int max) + { + Character = character; + Minimum = min; + Maximum = max; } - class EmphasisTestDescriptor + public EmphasisTestDescriptor(char character, int min, int max, params Tag[] tags) + : this(character, min, max) { - public char Character; - public int Minimum; - public int Maximum; - public Dictionary Tags = new Dictionary(); - - private EmphasisTestDescriptor(char character, int min, int max) + Debug.Assert(tags.Length == max - min + 1); + foreach (var tag in tags) { - Character = character; - Minimum = min; - Maximum = max; + Tags.Add(min++, tag); } - public EmphasisTestDescriptor(char character, int min, int max, params Tag[] tags) - : this(character, min, max) - { - Debug.Assert(tags.Length == max - min + 1); - foreach (var tag in tags) - { - Tags.Add(min++, tag); - } - } - public EmphasisTestDescriptor(char character, int min, int max, string tag) - : this(character, min, max, new Tag(tag)) { } } - class CustomEmphasisInline : EmphasisInline { } - static readonly EmphasisTestDescriptor[] EmphasisTestDescriptors = new[] - { - // Min Max - new EmphasisTestDescriptor('"', 1, 1, "quotation"), - new EmphasisTestDescriptor(',', 1, 2, "comma", "extra-comma"), - new EmphasisTestDescriptor('!', 2, 3, "warning", "error"), - new EmphasisTestDescriptor('=', 1, 3, "equal", "really-equal", "congruent"), - new EmphasisTestDescriptor('1', 1, 1, "one-only"), - new EmphasisTestDescriptor('2', 2, 2, "two-only"), - new EmphasisTestDescriptor('3', 3, 3, "three-only"), - }; + public EmphasisTestDescriptor(char character, int min, int max, string tag) + : this(character, min, max, new Tag(tag)) { } + } + class CustomEmphasisInline : EmphasisInline { } + static readonly EmphasisTestDescriptor[] EmphasisTestDescriptors = new[] + { + // Min Max + new EmphasisTestDescriptor('"', 1, 1, "quotation"), + new EmphasisTestDescriptor(',', 1, 2, "comma", "extra-comma"), + new EmphasisTestDescriptor('!', 2, 3, "warning", "error"), + new EmphasisTestDescriptor('=', 1, 3, "equal", "really-equal", "congruent"), + new EmphasisTestDescriptor('1', 1, 1, "one-only"), + new EmphasisTestDescriptor('2', 2, 2, "two-only"), + new EmphasisTestDescriptor('3', 3, 3, "three-only"), + }; - static readonly MarkdownPipeline Pipeline = new MarkdownPipelineBuilder().Use().Build(); + static readonly MarkdownPipeline Pipeline = new MarkdownPipelineBuilder().Use().Build(); - [Test] - [TestCase("*foo**", "foo*")] - [TestCase("**foo*", "*foo")] - [TestCase("***foo***", "foo")] - [TestCase("**_foo_**", "foo")] - [TestCase("_**foo**_", "foo")] - [TestCase("\"foo\"", "foo")] - [TestCase("\"\"foo\"\"", "foo")] - [TestCase("\"foo\"\"", "foo"")] - [TestCase("\"\"foo\"", ""foo")] - [TestCase(", foo", ", foo")] - [TestCase(", foo,", ", foo,")] - [TestCase(",some, foo,", "some foo,")] - [TestCase(",,foo,,", "foo")] - [TestCase(",foo,,", "foo,")] - [TestCase(",,,foo,,,", "foo")] - [TestCase("*foo*&_foo_", "foo&foo")] - [TestCase("!1!", "!1!")] - [TestCase("!!2!!", "2")] - [TestCase("!!!3!!!", "3")] - [TestCase("!!!34!!!!", "34!")] - [TestCase("!!!!43!!!", "!43")] - [TestCase("!!!!44!!!!", "!44!")] // This is a new case - should the second ! be before or after ? - [TestCase("!!!!!5!!!!!", "5")] - [TestCase("!!!!!!6!!!!!!", "6")] - [TestCase("!! !mixed!!!", "!! !mixed!!!")] // can't open the delimiter because of the whitespace - [TestCase("=", "=")] - [TestCase("==", "==")] - [TestCase("====", "====")] - [TestCase("=a", "=a")] - [TestCase("=a=", "a")] - [TestCase("==a=", "=a")] - [TestCase("==a==", "a")] - [TestCase("==a===", "a=")] - [TestCase("===a===", "a")] - [TestCase("====a====", "a")] - [TestCase("=====a=====", "a")] - [TestCase("1", "1")] - [TestCase("1 1", "1 1")] - [TestCase("1Foo1", "Foo")] - [TestCase("1121", "12")] - [TestCase("22322", "3")] - [TestCase("2232", "2232")] - [TestCase("333", "333")] - [TestCase("3334333", "4")] - [TestCase("33334333", "34")] - [TestCase("33343333", "43")] - [TestCase("122122", "2222")] - [TestCase("221221", "11")] - [TestCase("122foo221", "foo")] - [TestCase("122foo122", "22foo22")] - [TestCase("!!!!!Attention:!! \"==1+1== 2\",but ===333 and 222===, mod 111!!!", - "Attention: + 2but 333 and 222 mod 111")] - public void TestEmphasis(string markdown, string expectedHtml) - { - TestParser.TestSpec(markdown, "

    " + expectedHtml + "

    ", Pipeline); - } + [Test] + [TestCase("*foo**", "foo*")] + [TestCase("**foo*", "*foo")] + [TestCase("***foo***", "foo")] + [TestCase("**_foo_**", "foo")] + [TestCase("_**foo**_", "foo")] + [TestCase("\"foo\"", "foo")] + [TestCase("\"\"foo\"\"", "foo")] + [TestCase("\"foo\"\"", "foo"")] + [TestCase("\"\"foo\"", ""foo")] + [TestCase(", foo", ", foo")] + [TestCase(", foo,", ", foo,")] + [TestCase(",some, foo,", "some foo,")] + [TestCase(",,foo,,", "foo")] + [TestCase(",foo,,", "foo,")] + [TestCase(",,,foo,,,", "foo")] + [TestCase("*foo*&_foo_", "foo&foo")] + [TestCase("!1!", "!1!")] + [TestCase("!!2!!", "2")] + [TestCase("!!!3!!!", "3")] + [TestCase("!!!34!!!!", "34!")] + [TestCase("!!!!43!!!", "!43")] + [TestCase("!!!!44!!!!", "!44!")] // This is a new case - should the second ! be before or after ? + [TestCase("!!!!!5!!!!!", "5")] + [TestCase("!!!!!!6!!!!!!", "6")] + [TestCase("!! !mixed!!!", "!! !mixed!!!")] // can't open the delimiter because of the whitespace + [TestCase("=", "=")] + [TestCase("==", "==")] + [TestCase("====", "====")] + [TestCase("=a", "=a")] + [TestCase("=a=", "a")] + [TestCase("==a=", "=a")] + [TestCase("==a==", "a")] + [TestCase("==a===", "a=")] + [TestCase("===a===", "a")] + [TestCase("====a====", "a")] + [TestCase("=====a=====", "a")] + [TestCase("1", "1")] + [TestCase("1 1", "1 1")] + [TestCase("1Foo1", "Foo")] + [TestCase("1121", "12")] + [TestCase("22322", "3")] + [TestCase("2232", "2232")] + [TestCase("333", "333")] + [TestCase("3334333", "4")] + [TestCase("33334333", "34")] + [TestCase("33343333", "43")] + [TestCase("122122", "2222")] + [TestCase("221221", "11")] + [TestCase("122foo221", "foo")] + [TestCase("122foo122", "22foo22")] + [TestCase("!!!!!Attention:!! \"==1+1== 2\",but ===333 and 222===, mod 111!!!", + "Attention: + 2but 333 and 222 mod 111")] + public void TestEmphasis(string markdown, string expectedHtml) + { + TestParser.TestSpec(markdown, "

    " + expectedHtml + "

    ", Pipeline); } } diff --git a/src/Markdig.Tests/TestEmphasisExtraOptions.cs b/src/Markdig.Tests/TestEmphasisExtraOptions.cs index 7da3fec7d..217987f3a 100644 --- a/src/Markdig.Tests/TestEmphasisExtraOptions.cs +++ b/src/Markdig.Tests/TestEmphasisExtraOptions.cs @@ -1,45 +1,43 @@ -using NUnit.Framework; using Markdig.Extensions.EmphasisExtras; -namespace Markdig.Tests +namespace Markdig.Tests; + +[TestFixture] +public class TestEmphasisExtraOptions { - [TestFixture] - public class TestEmphasisExtraOptions + [Test] + public void OnlyStrikethrough_Single() { - [Test] - public void OnlyStrikethrough_Single() - { - TestParser.TestSpec("~foo~", "

    ~foo~

    ", new MarkdownPipelineBuilder().UseEmphasisExtras(EmphasisExtraOptions.Strikethrough).Build()); - } + TestParser.TestSpec("~foo~", "

    ~foo~

    ", new MarkdownPipelineBuilder().UseEmphasisExtras(EmphasisExtraOptions.Strikethrough).Build()); + } - [Test] - public void OnlyStrikethrough_Double() - { - TestParser.TestSpec("~~foo~~", "

    foo

    ", new MarkdownPipelineBuilder().UseEmphasisExtras(EmphasisExtraOptions.Strikethrough).Build()); - } + [Test] + public void OnlyStrikethrough_Double() + { + TestParser.TestSpec("~~foo~~", "

    foo

    ", new MarkdownPipelineBuilder().UseEmphasisExtras(EmphasisExtraOptions.Strikethrough).Build()); + } - [Test] - public void OnlySubscript_Single() - { - TestParser.TestSpec("~foo~", "

    foo

    ", new MarkdownPipelineBuilder().UseEmphasisExtras(EmphasisExtraOptions.Subscript).Build()); - } + [Test] + public void OnlySubscript_Single() + { + TestParser.TestSpec("~foo~", "

    foo

    ", new MarkdownPipelineBuilder().UseEmphasisExtras(EmphasisExtraOptions.Subscript).Build()); + } - [Test] - public void OnlySubscript_Double() - { - TestParser.TestSpec("~~foo~~", "

    foo

    ", new MarkdownPipelineBuilder().UseEmphasisExtras(EmphasisExtraOptions.Subscript).Build()); - } + [Test] + public void OnlySubscript_Double() + { + TestParser.TestSpec("~~foo~~", "

    foo

    ", new MarkdownPipelineBuilder().UseEmphasisExtras(EmphasisExtraOptions.Subscript).Build()); + } - [Test] - public void SubscriptAndStrikethrough_Single() - { - TestParser.TestSpec("~foo~", "

    foo

    ", new MarkdownPipelineBuilder().UseEmphasisExtras(EmphasisExtraOptions.Strikethrough | EmphasisExtraOptions.Subscript).Build()); - } + [Test] + public void SubscriptAndStrikethrough_Single() + { + TestParser.TestSpec("~foo~", "

    foo

    ", new MarkdownPipelineBuilder().UseEmphasisExtras(EmphasisExtraOptions.Strikethrough | EmphasisExtraOptions.Subscript).Build()); + } - [Test] - public void SubscriptAndStrikethrough_Double() - { - TestParser.TestSpec("~~foo~~", "

    foo

    ", new MarkdownPipelineBuilder().UseEmphasisExtras(EmphasisExtraOptions.Strikethrough | EmphasisExtraOptions.Subscript).Build()); - } + [Test] + public void SubscriptAndStrikethrough_Double() + { + TestParser.TestSpec("~~foo~~", "

    foo

    ", new MarkdownPipelineBuilder().UseEmphasisExtras(EmphasisExtraOptions.Strikethrough | EmphasisExtraOptions.Subscript).Build()); } } \ No newline at end of file diff --git a/src/Markdig.Tests/TestEmphasisPlus.cs b/src/Markdig.Tests/TestEmphasisPlus.cs index 4822f543e..9c2fdc776 100644 --- a/src/Markdig.Tests/TestEmphasisPlus.cs +++ b/src/Markdig.Tests/TestEmphasisPlus.cs @@ -1,23 +1,21 @@ // 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 NUnit.Framework; -namespace Markdig.Tests +namespace Markdig.Tests; + +[TestFixture] +public partial class TestEmphasisPlus { - [TestFixture] - public partial class TestEmphasisPlus + [Test] + public void StrongNormal() { - [Test] - public void StrongNormal() - { - TestParser.TestSpec("***Strong emphasis*** normal", "

    Strong emphasis normal

    ", ""); - } + TestParser.TestSpec("***Strong emphasis*** normal", "

    Strong emphasis normal

    ", ""); + } - [Test] - public void NormalStrongNormal() - { - TestParser.TestSpec("normal ***Strong emphasis*** normal", "

    normal Strong emphasis normal

    ", ""); - } + [Test] + public void NormalStrongNormal() + { + TestParser.TestSpec("normal ***Strong emphasis*** normal", "

    normal Strong emphasis normal

    ", ""); } } \ No newline at end of file diff --git a/src/Markdig.Tests/TestExceptionNotThrown.cs b/src/Markdig.Tests/TestExceptionNotThrown.cs index 034d2620c..0663017c1 100644 --- a/src/Markdig.Tests/TestExceptionNotThrown.cs +++ b/src/Markdig.Tests/TestExceptionNotThrown.cs @@ -1,48 +1,45 @@ -using NUnit.Framework; +namespace Markdig.Tests; -namespace Markdig.Tests +[TestFixture] +public class TestExceptionNotThrown { - [TestFixture] - public class TestExceptionNotThrown + [Test] + public void DoesNotThrowIndexOutOfRangeException1() { - [Test] - public void DoesNotThrowIndexOutOfRangeException1() + Assert.DoesNotThrow(() => { - Assert.DoesNotThrow(() => - { - var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build(); - Markdown.ToHtml("+-\n|\n+", pipeline); - }); - } + var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build(); + Markdown.ToHtml("+-\n|\n+", pipeline); + }); + } - [Test] - public void DoesNotThrowIndexOutOfRangeException2() + [Test] + public void DoesNotThrowIndexOutOfRangeException2() + { + Assert.DoesNotThrow(() => { - Assert.DoesNotThrow(() => - { - var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build(); - Markdown.ToHtml("+--\n|\n+0", pipeline); - }); - } + var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build(); + Markdown.ToHtml("+--\n|\n+0", pipeline); + }); + } - [Test] - public void DoesNotThrowIndexOutOfRangeException3() + [Test] + public void DoesNotThrowIndexOutOfRangeException3() + { + Assert.DoesNotThrow(() => { - Assert.DoesNotThrow(() => - { - var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build(); - Markdown.ToHtml("+-\n|\n+\n0", pipeline); - }); - } + var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build(); + Markdown.ToHtml("+-\n|\n+\n0", pipeline); + }); + } - [Test] - public void DoesNotThrowIndexOutOfRangeException4() + [Test] + public void DoesNotThrowIndexOutOfRangeException4() + { + Assert.DoesNotThrow(() => { - Assert.DoesNotThrow(() => - { - var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build(); - Markdown.ToHtml("+-\n|\n+0", pipeline); - }); - } + var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build(); + Markdown.ToHtml("+-\n|\n+0", pipeline); + }); } } diff --git a/src/Markdig.Tests/TestFastStringWriter.cs b/src/Markdig.Tests/TestFastStringWriter.cs index d43dd3a3d..e28b0a60b 100644 --- a/src/Markdig.Tests/TestFastStringWriter.cs +++ b/src/Markdig.Tests/TestFastStringWriter.cs @@ -1,189 +1,186 @@ -using Markdig.Helpers; -using NUnit.Framework; -using System; using System.Text; -using System.Threading.Tasks; -namespace Markdig.Tests +using Markdig.Helpers; + +namespace Markdig.Tests; + +[TestFixture] +public class TestFastStringWriter { - [TestFixture] - public class TestFastStringWriter - { - private const string NewLineReplacement = "~~NEW_LINE~~"; + private const string NewLineReplacement = "~~NEW_LINE~~"; - private FastStringWriter _writer = new(); + private FastStringWriter _writer = new(); - [SetUp] - public void Setup() + [SetUp] + public void Setup() + { + _writer = new FastStringWriter { - _writer = new FastStringWriter - { - NewLine = NewLineReplacement - }; - } + NewLine = NewLineReplacement + }; + } - public void AssertToString(string value) - { - value = value.Replace("\n", NewLineReplacement); - Assert.AreEqual(value, _writer.ToString()); - Assert.AreEqual(value, _writer.ToString()); - } + 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); + [Test] + public async Task NewLine() + { + Assert.AreEqual("\n", new FastStringWriter().NewLine); - _writer.NewLine = "\r"; - Assert.AreEqual("\r", _writer.NewLine); + _writer.NewLine = "\r"; + Assert.AreEqual("\r", _writer.NewLine); - _writer.NewLine = "foo"; - Assert.AreEqual("foo", _writer.NewLine); + _writer.NewLine = "foo"; + Assert.AreEqual("foo", _writer.NewLine); - _writer.WriteLine(); - await _writer.WriteLineAsync(); - _writer.WriteLine("bar"); - Assert.AreEqual("foofoobarfoo", _writer.ToString()); - } + _writer.WriteLine(); + await _writer.WriteLineAsync(); + _writer.WriteLine("bar"); + Assert.AreEqual("foofoobarfoo", _writer.ToString()); + } - [Test] - public async Task FlushCloseDispose() - { - _writer.Write('a'); + [Test] + public async Task FlushCloseDispose() + { + _writer.Write('a'); - // Nops - _writer.Close(); - _writer.Dispose(); - await _writer.DisposeAsync(); - _writer.Flush(); - await _writer.FlushAsync(); + // Nops + _writer.Close(); + _writer.Dispose(); + await _writer.DisposeAsync(); + _writer.Flush(); + await _writer.FlushAsync(); - _writer.Write('b'); - AssertToString("ab"); - } + _writer.Write('b'); + AssertToString("ab"); + } - [Test] - public async Task Write_Char() - { - _writer.Write('a'); - AssertToString("a"); + [Test] + public async Task Write_Char() + { + _writer.Write('a'); + AssertToString("a"); - _writer.Write('b'); - AssertToString("ab"); + _writer.Write('b'); + AssertToString("ab"); - _writer.Write('\0'); - _writer.Write('\r'); - _writer.Write('\u1234'); - AssertToString("ab\0\r\u1234"); + _writer.Write('\0'); + _writer.Write('\r'); + _writer.Write('\u1234'); + AssertToString("ab\0\r\u1234"); - _writer.Reset(); - AssertToString(""); + _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'); - _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)); - } + AssertToString(new string('a', i + 1)); } + } - [Test] - public async Task Write_String() - { - _writer.Write("foo"); - AssertToString("foo"); + [Test] + public async Task Write_String() + { + _writer.Write("foo"); + AssertToString("foo"); - _writer.WriteLine("bar"); - AssertToString("foobar\n"); + _writer.WriteLine("bar"); + AssertToString("foobar\n"); - await _writer.WriteAsync("baz"); - await _writer.WriteLineAsync("foo"); - AssertToString("foobar\nbazfoo\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)); - } + _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"); + [Test] + public async Task Write_Span() + { + _writer.Write("foo".AsSpan()); + AssertToString("foo"); - _writer.WriteLine("bar".AsSpan()); - AssertToString("foobar\n"); + _writer.WriteLine("bar".AsSpan()); + AssertToString("foobar\n"); - await _writer.WriteAsync("baz".AsMemory()); - await _writer.WriteLineAsync("foo".AsMemory()); - AssertToString("foobar\nbazfoo\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)); - } + _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"); + [Test] + public async Task Write_CharArray() + { + _writer.Write("foo".ToCharArray()); + AssertToString("foo"); - _writer.WriteLine("bar".ToCharArray()); - AssertToString("foobar\n"); + _writer.WriteLine("bar".ToCharArray()); + AssertToString("foobar\n"); - await _writer.WriteAsync("baz".ToCharArray()); - await _writer.WriteLineAsync("foo".ToCharArray()); - AssertToString("foobar\nbazfoo\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)); - } + _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"); + [Test] + public async Task Write_CharArrayWithIndexes() + { + _writer.Write("foo".ToCharArray(), 1, 1); + AssertToString("o"); - _writer.WriteLine("bar".ToCharArray(), 0, 2); - AssertToString("oba\n"); + _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"); + 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)); - } + _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"); + [Test] + public async Task Write_StringBuilder() + { + _writer.Write(new StringBuilder("foo")); + AssertToString("foo"); - _writer.WriteLine(new StringBuilder("bar")); - AssertToString("foobar\n"); + _writer.WriteLine(new StringBuilder("bar")); + AssertToString("foobar\n"); - await _writer.WriteAsync(new StringBuilder("baz")); - await _writer.WriteLineAsync(new StringBuilder("foo")); - AssertToString("foobar\nbazfoo\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)); - } + var sb = new StringBuilder("foo"); + sb.Append('a', 1050); + _writer.Write(sb); + AssertToString("foobar\nbazfoo\nfoo" + new string('a', 1050)); } } diff --git a/src/Markdig.Tests/TestFencedCodeBlocks.cs b/src/Markdig.Tests/TestFencedCodeBlocks.cs index 0f7b81661..d6bd7236c 100644 --- a/src/Markdig.Tests/TestFencedCodeBlocks.cs +++ b/src/Markdig.Tests/TestFencedCodeBlocks.cs @@ -1,46 +1,43 @@ -using System.Linq; using Markdig.Syntax; -using NUnit.Framework; -namespace Markdig.Tests +namespace Markdig.Tests; + +public class TestFencedCodeBlocks { - 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] - [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('~'); + Test('`'); + Test('~'); - void Test(char fencedChar) - { - const string Contents = "Foo\nBar\n"; + 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"; + var fence = new string(fencedChar, 3); + string markdownText = $"{fence}{infoString}\n{Contents}\n{fence}\n"; - MarkdownDocument document = Markdown.Parse(markdownText); + MarkdownDocument document = Markdown.Parse(markdownText); - FencedCodeBlock codeBlock = document.Descendants().Single(); + FencedCodeBlock codeBlock = document.Descendants().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()); - } + 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()); } } } diff --git a/src/Markdig.Tests/TestHtmlAttributes.cs b/src/Markdig.Tests/TestHtmlAttributes.cs index 2fe2895cd..8737931f9 100644 --- a/src/Markdig.Tests/TestHtmlAttributes.cs +++ b/src/Markdig.Tests/TestHtmlAttributes.cs @@ -3,94 +3,91 @@ // See the license.txt file in the project root for more information. using Markdig.Renderers.Html; -using NUnit.Framework; -using System.Collections.Generic; -namespace Markdig.Tests +namespace Markdig.Tests; + +[TestFixture()] +public class TestHtmlAttributes { - [TestFixture()] - public class TestHtmlAttributes + [Test] + public void TestAddClass() { - [Test] - public void TestAddClass() - { - var attributes = new HtmlAttributes(); - attributes.AddClass("test"); - Assert.NotNull(attributes.Classes); - Assert.AreEqual(new List() { "test" }, attributes.Classes); + var attributes = new HtmlAttributes(); + attributes.AddClass("test"); + Assert.NotNull(attributes.Classes); + Assert.AreEqual(new List() { "test" }, attributes.Classes); - attributes.AddClass("test"); - Assert.AreEqual(1, attributes.Classes.Count); + attributes.AddClass("test"); + Assert.AreEqual(1, attributes.Classes.Count); - attributes.AddClass("test1"); - Assert.AreEqual(new List() { "test", "test1" }, attributes.Classes); - } + attributes.AddClass("test1"); + Assert.AreEqual(new List() { "test", "test1" }, attributes.Classes); + } - [Test] - public void TestAddProperty() - { - var attributes = new HtmlAttributes(); - attributes.AddProperty("key1", "1"); - Assert.NotNull(attributes.Properties); - Assert.AreEqual(new List>() { new KeyValuePair("key1", "1") }, attributes.Properties); + [Test] + public void TestAddProperty() + { + var attributes = new HtmlAttributes(); + attributes.AddProperty("key1", "1"); + Assert.NotNull(attributes.Properties); + Assert.AreEqual(new List>() { new KeyValuePair("key1", "1") }, attributes.Properties); - attributes.AddPropertyIfNotExist("key1", "1"); - Assert.NotNull(attributes.Properties); - Assert.AreEqual(new List>() { new KeyValuePair("key1", "1") }, attributes.Properties); + attributes.AddPropertyIfNotExist("key1", "1"); + Assert.NotNull(attributes.Properties); + Assert.AreEqual(new List>() { new KeyValuePair("key1", "1") }, attributes.Properties); - attributes.AddPropertyIfNotExist("key2", "2"); - Assert.AreEqual(new List>() { new KeyValuePair("key1", "1"), new KeyValuePair("key2", "2") }, attributes.Properties); - } + attributes.AddPropertyIfNotExist("key2", "2"); + Assert.AreEqual(new List>() { new KeyValuePair("key1", "1"), new KeyValuePair("key2", "2") }, attributes.Properties); + } - [Test] - public void TestCopyTo() - { - var from = new HtmlAttributes(); - from.AddClass("test"); - from.AddProperty("key1", "1"); + [Test] + public void TestCopyTo() + { + var from = new HtmlAttributes(); + from.AddClass("test"); + from.AddProperty("key1", "1"); - var to = new HtmlAttributes(); - from.CopyTo(to); + var to = new HtmlAttributes(); + from.CopyTo(to); - Assert.True(ReferenceEquals(from.Classes, to.Classes)); - Assert.True(ReferenceEquals(from.Properties, to.Properties)); + Assert.True(ReferenceEquals(from.Classes, to.Classes)); + Assert.True(ReferenceEquals(from.Properties, to.Properties)); - // From: Classes From: Properties To: Classes To: Properties - // test1: null null null null - from = new HtmlAttributes(); - to = new HtmlAttributes(); - from.CopyTo(to, false, false); - Assert.Null(to.Classes); - Assert.Null(to.Properties); + // From: Classes From: Properties To: Classes To: Properties + // test1: null null null null + from = new HtmlAttributes(); + to = new HtmlAttributes(); + from.CopyTo(to, false, false); + Assert.Null(to.Classes); + Assert.Null(to.Properties); - // test2: ["test"] ["key1", "1"] null null - from = new HtmlAttributes(); - to = new HtmlAttributes(); - from.AddClass("test"); - from.AddProperty("key1", "1"); - from.CopyTo(to, false, false); - Assert.AreEqual(new List() { "test" }, to.Classes); - Assert.AreEqual(new List>() { new KeyValuePair("key1", "1")}, to.Properties); + // test2: ["test"] ["key1", "1"] null null + from = new HtmlAttributes(); + to = new HtmlAttributes(); + from.AddClass("test"); + from.AddProperty("key1", "1"); + from.CopyTo(to, false, false); + Assert.AreEqual(new List() { "test" }, to.Classes); + Assert.AreEqual(new List>() { new KeyValuePair("key1", "1")}, to.Properties); - // test3: null null ["test"] ["key1", "1"] - from = new HtmlAttributes(); - to = new HtmlAttributes(); - to.AddClass("test"); - to.AddProperty("key1", "1"); - from.CopyTo(to, false, false); - Assert.AreEqual(new List() { "test" }, to.Classes); - Assert.AreEqual(new List>() { new KeyValuePair("key1", "1") }, to.Properties); + // test3: null null ["test"] ["key1", "1"] + from = new HtmlAttributes(); + to = new HtmlAttributes(); + to.AddClass("test"); + to.AddProperty("key1", "1"); + from.CopyTo(to, false, false); + Assert.AreEqual(new List() { "test" }, to.Classes); + Assert.AreEqual(new List>() { new KeyValuePair("key1", "1") }, to.Properties); - // test4: ["test1"] ["key2", "2"] ["test"] ["key1", "1"] - from = new HtmlAttributes(); - to = new HtmlAttributes(); - from.AddClass("test1"); - from.AddProperty("key2", "2"); - to.AddClass("test"); - to.AddProperty("key1", "1"); - from.CopyTo(to, false, false); - Assert.AreEqual(new List() { "test", "test1" }, to.Classes); - Assert.AreEqual(new List>() { new KeyValuePair("key1", "1"), new KeyValuePair("key2", "2") }, to.Properties); - } + // test4: ["test1"] ["key2", "2"] ["test"] ["key1", "1"] + from = new HtmlAttributes(); + to = new HtmlAttributes(); + from.AddClass("test1"); + from.AddProperty("key2", "2"); + to.AddClass("test"); + to.AddProperty("key1", "1"); + from.CopyTo(to, false, false); + Assert.AreEqual(new List() { "test", "test1" }, to.Classes); + Assert.AreEqual(new List>() { new KeyValuePair("key1", "1"), new KeyValuePair("key2", "2") }, to.Properties); } } \ No newline at end of file diff --git a/src/Markdig.Tests/TestHtmlHelper.cs b/src/Markdig.Tests/TestHtmlHelper.cs index d6e788edd..51a8fdbf3 100644 --- a/src/Markdig.Tests/TestHtmlHelper.cs +++ b/src/Markdig.Tests/TestHtmlHelper.cs @@ -1,30 +1,28 @@ // 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 NUnit.Framework; using Markdig.Helpers; -namespace Markdig.Tests +namespace Markdig.Tests; + +[TestFixture] +public class TestHtmlHelper { - [TestFixture] - public class TestHtmlHelper + [Test] + public void TestParseHtmlTagSimple() { - [Test] - public void TestParseHtmlTagSimple() - { - var inputTag = ""; - var text = new StringSlice(inputTag); - Assert.True(HtmlHelper.TryParseHtmlTag(ref text, out string outputTag)); - Assert.AreEqual(inputTag, outputTag); - } + var inputTag = ""; + var text = new StringSlice(inputTag); + Assert.True(HtmlHelper.TryParseHtmlTag(ref text, out string outputTag)); + Assert.AreEqual(inputTag, outputTag); + } - [Test] - public void TestParseHtmlTagSimpleWithAttribute() - { - var inputTag = ""; - var text = new StringSlice(inputTag); - Assert.True(HtmlHelper.TryParseHtmlTag(ref text, out string outputTag)); - Assert.AreEqual(inputTag, outputTag); - } + [Test] + public void TestParseHtmlTagSimpleWithAttribute() + { + var inputTag = ""; + var text = new StringSlice(inputTag); + Assert.True(HtmlHelper.TryParseHtmlTag(ref text, out string outputTag)); + Assert.AreEqual(inputTag, outputTag); } } \ No newline at end of file diff --git a/src/Markdig.Tests/TestImageAltText.cs b/src/Markdig.Tests/TestImageAltText.cs index f8a0ad2e6..869178d59 100644 --- a/src/Markdig.Tests/TestImageAltText.cs +++ b/src/Markdig.Tests/TestImageAltText.cs @@ -1,25 +1,23 @@ -using NUnit.Framework; using System.Text.RegularExpressions; -namespace Markdig.Tests +namespace Markdig.Tests; + +[TestFixture] +public class TestImageAltText { - [TestFixture] - public class TestImageAltText + [Test] + [TestCase("![](image.jpg)", "")] + [TestCase("![foo](image.jpg)", "foo")] + [TestCase("![][1]\n\n[1]: image.jpg", "")] + [TestCase("![bar][1]\n\n[1]: image.jpg", "bar")] + [TestCase("![](image.jpg 'title')", "")] + [TestCase("![foo](image.jpg 'title')", "foo")] + [TestCase("![][1]\n\n[1]: image.jpg 'title'", "")] + [TestCase("![bar][1]\n\n[1]: image.jpg 'title'", "bar")] + public void TestImageHtmlAltText(string markdown, string expectedAltText) { - [Test] - [TestCase("![](image.jpg)", "")] - [TestCase("![foo](image.jpg)", "foo")] - [TestCase("![][1]\n\n[1]: image.jpg", "")] - [TestCase("![bar][1]\n\n[1]: image.jpg", "bar")] - [TestCase("![](image.jpg 'title')", "")] - [TestCase("![foo](image.jpg 'title')", "foo")] - [TestCase("![][1]\n\n[1]: image.jpg 'title'", "")] - [TestCase("![bar][1]\n\n[1]: image.jpg 'title'", "bar")] - public void TestImageHtmlAltText(string markdown, string expectedAltText) - { - string html = Markdown.ToHtml(markdown); - string actualAltText = Regex.Match(html, "alt=\"(.*?)\"").Groups[1].Value; - Assert.AreEqual(expectedAltText, actualAltText); - } + string html = Markdown.ToHtml(markdown); + string actualAltText = Regex.Match(html, "alt=\"(.*?)\"").Groups[1].Value; + Assert.AreEqual(expectedAltText, actualAltText); } } diff --git a/src/Markdig.Tests/TestLazySubstring.cs b/src/Markdig.Tests/TestLazySubstring.cs index d92b4b6c3..523c1506c 100644 --- a/src/Markdig.Tests/TestLazySubstring.cs +++ b/src/Markdig.Tests/TestLazySubstring.cs @@ -1,67 +1,65 @@ using Markdig.Helpers; -using NUnit.Framework; -namespace Markdig.Tests +namespace Markdig.Tests; + +public class TestLazySubstring { - public class TestLazySubstring + [Theory] + [TestCase("")] + [TestCase("a")] + [TestCase("foo")] + public void LazySubstring_ReturnsCorrectSubstring(string text) { - [Theory] - [TestCase("")] - [TestCase("a")] - [TestCase("foo")] - public void LazySubstring_ReturnsCorrectSubstring(string text) - { - var substring = new LazySubstring(text); - Assert.AreEqual(0, substring.Offset); - Assert.AreEqual(text.Length, substring.Length); + var substring = new LazySubstring(text); + Assert.AreEqual(0, substring.Offset); + Assert.AreEqual(text.Length, substring.Length); - Assert.AreEqual(text, substring.AsSpan().ToString()); - Assert.AreEqual(text, substring.AsSpan().ToString()); - Assert.AreEqual(0, substring.Offset); - Assert.AreEqual(text.Length, substring.Length); + Assert.AreEqual(text, substring.AsSpan().ToString()); + Assert.AreEqual(text, substring.AsSpan().ToString()); + Assert.AreEqual(0, substring.Offset); + Assert.AreEqual(text.Length, substring.Length); - Assert.AreSame(substring.ToString(), substring.ToString()); - Assert.AreEqual(text, substring.ToString()); - Assert.AreEqual(0, substring.Offset); - Assert.AreEqual(text.Length, substring.Length); + Assert.AreSame(substring.ToString(), substring.ToString()); + Assert.AreEqual(text, substring.ToString()); + Assert.AreEqual(0, substring.Offset); + Assert.AreEqual(text.Length, substring.Length); - Assert.AreEqual(text, substring.AsSpan().ToString()); - Assert.AreEqual(text, substring.AsSpan().ToString()); - Assert.AreEqual(0, substring.Offset); - Assert.AreEqual(text.Length, substring.Length); - } + Assert.AreEqual(text, substring.AsSpan().ToString()); + Assert.AreEqual(text, substring.AsSpan().ToString()); + Assert.AreEqual(0, substring.Offset); + Assert.AreEqual(text.Length, substring.Length); + } - [Theory] - [TestCase("", 0, 0)] - [TestCase("a", 0, 0)] - [TestCase("a", 1, 0)] - [TestCase("a", 0, 1)] - [TestCase("foo", 1, 0)] - [TestCase("foo", 1, 1)] - [TestCase("foo", 1, 2)] - [TestCase("foo", 0, 3)] - public void LazySubstring_ReturnsCorrectSubstring(string text, int start, int length) - { - var substring = new LazySubstring(text, start, length); - Assert.AreEqual(start, substring.Offset); - Assert.AreEqual(length, substring.Length); + [Theory] + [TestCase("", 0, 0)] + [TestCase("a", 0, 0)] + [TestCase("a", 1, 0)] + [TestCase("a", 0, 1)] + [TestCase("foo", 1, 0)] + [TestCase("foo", 1, 1)] + [TestCase("foo", 1, 2)] + [TestCase("foo", 0, 3)] + public void LazySubstring_ReturnsCorrectSubstring(string text, int start, int length) + { + var substring = new LazySubstring(text, start, length); + Assert.AreEqual(start, substring.Offset); + Assert.AreEqual(length, substring.Length); - string expectedSubstring = text.Substring(start, length); + string expectedSubstring = text.Substring(start, length); - Assert.AreEqual(expectedSubstring, substring.AsSpan().ToString()); - Assert.AreEqual(expectedSubstring, substring.AsSpan().ToString()); - Assert.AreEqual(start, substring.Offset); - Assert.AreEqual(length, substring.Length); + Assert.AreEqual(expectedSubstring, substring.AsSpan().ToString()); + Assert.AreEqual(expectedSubstring, substring.AsSpan().ToString()); + Assert.AreEqual(start, substring.Offset); + Assert.AreEqual(length, substring.Length); - Assert.AreSame(substring.ToString(), substring.ToString()); - Assert.AreEqual(expectedSubstring, substring.ToString()); - Assert.AreEqual(0, substring.Offset); - Assert.AreEqual(length, substring.Length); + Assert.AreSame(substring.ToString(), substring.ToString()); + Assert.AreEqual(expectedSubstring, substring.ToString()); + Assert.AreEqual(0, substring.Offset); + Assert.AreEqual(length, substring.Length); - Assert.AreEqual(expectedSubstring, substring.AsSpan().ToString()); - Assert.AreEqual(expectedSubstring, substring.AsSpan().ToString()); - Assert.AreEqual(0, substring.Offset); - Assert.AreEqual(length, substring.Length); - } + Assert.AreEqual(expectedSubstring, substring.AsSpan().ToString()); + Assert.AreEqual(expectedSubstring, substring.AsSpan().ToString()); + Assert.AreEqual(0, substring.Offset); + Assert.AreEqual(length, substring.Length); } } diff --git a/src/Markdig.Tests/TestLineReader.cs b/src/Markdig.Tests/TestLineReader.cs index 342c4c0de..abd108ac0 100644 --- a/src/Markdig.Tests/TestLineReader.cs +++ b/src/Markdig.Tests/TestLineReader.cs @@ -2,126 +2,124 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using NUnit.Framework; using Markdig.Helpers; -namespace Markdig.Tests +namespace Markdig.Tests; + +/// +/// Test for . +/// +[TestFixture] +public class TestLineReader { - /// - /// Test for . - /// - [TestFixture] - public class TestLineReader + [Test] + public void TestEmpty() { - [Test] - public void TestEmpty() - { - var lineReader = new LineReader(""); - Assert.Null(lineReader.ReadLine().Text); - } + var lineReader = new LineReader(""); + Assert.Null(lineReader.ReadLine().Text); + } - [Test] - public void TestLinesOnlyLf() - { - var lineReader = new LineReader("\n\n\n"); - Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString()); - Assert.AreEqual(1, lineReader.SourcePosition); - Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString()); - Assert.AreEqual(2, lineReader.SourcePosition); - Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString()); - Assert.Null(lineReader.ReadLine().Text); - } + [Test] + public void TestLinesOnlyLf() + { + var lineReader = new LineReader("\n\n\n"); + Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString()); + Assert.AreEqual(1, lineReader.SourcePosition); + Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString()); + Assert.AreEqual(2, lineReader.SourcePosition); + Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString()); + Assert.Null(lineReader.ReadLine().Text); + } - [Test] - public void TestLinesOnlyCr() - { - var lineReader = new LineReader("\r\r\r"); - Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString()); - Assert.AreEqual(1, lineReader.SourcePosition); - Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString()); - Assert.AreEqual(2, lineReader.SourcePosition); - Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString()); - Assert.Null(lineReader.ReadLine().Text); - } + [Test] + public void TestLinesOnlyCr() + { + var lineReader = new LineReader("\r\r\r"); + Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString()); + Assert.AreEqual(1, lineReader.SourcePosition); + Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString()); + Assert.AreEqual(2, lineReader.SourcePosition); + Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString()); + Assert.Null(lineReader.ReadLine().Text); + } - [Test] - public void TestLinesOnlyCrLf() - { - var lineReader = new LineReader("\r\n\r\n\r\n"); - Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString()); - Assert.AreEqual(2, lineReader.SourcePosition); - Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString()); - Assert.AreEqual(4, lineReader.SourcePosition); - Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString()); - Assert.Null(lineReader.ReadLine().Text); - } + [Test] + public void TestLinesOnlyCrLf() + { + var lineReader = new LineReader("\r\n\r\n\r\n"); + Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString()); + Assert.AreEqual(2, lineReader.SourcePosition); + Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString()); + Assert.AreEqual(4, lineReader.SourcePosition); + Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString()); + Assert.Null(lineReader.ReadLine().Text); + } - [Test] - public void TestNoEndOfLine() - { - var lineReader = new LineReader("123"); - Assert.AreEqual("123", lineReader.ReadLine().ToString()); - Assert.Null(lineReader.ReadLine().Text); - } + [Test] + public void TestNoEndOfLine() + { + var lineReader = new LineReader("123"); + Assert.AreEqual("123", lineReader.ReadLine().ToString()); + Assert.Null(lineReader.ReadLine().Text); + } - [Test] - public void TestLf() - { - var lineReader = new LineReader("123\n"); - Assert.AreEqual("123", lineReader.ReadLine().ToString()); - Assert.AreEqual(4, lineReader.SourcePosition); - Assert.Null(lineReader.ReadLine().Text); - } + [Test] + public void TestLf() + { + var lineReader = new LineReader("123\n"); + Assert.AreEqual("123", lineReader.ReadLine().ToString()); + Assert.AreEqual(4, lineReader.SourcePosition); + Assert.Null(lineReader.ReadLine().Text); + } - [Test] - public void TestLf2() - { - // When limited == true, we limit the internal buffer exactly after the first new line char '\n' - var lineReader = new LineReader("123\n456"); - Assert.AreEqual("123", lineReader.ReadLine().ToString()); - Assert.AreEqual(4, lineReader.SourcePosition); - Assert.AreEqual("456", lineReader.ReadLine().ToString()); - Assert.Null(lineReader.ReadLine().Text); - } + [Test] + public void TestLf2() + { + // When limited == true, we limit the internal buffer exactly after the first new line char '\n' + var lineReader = new LineReader("123\n456"); + Assert.AreEqual("123", lineReader.ReadLine().ToString()); + Assert.AreEqual(4, lineReader.SourcePosition); + Assert.AreEqual("456", lineReader.ReadLine().ToString()); + Assert.Null(lineReader.ReadLine().Text); + } - [Test] - public void TestCr() - { - var lineReader = new LineReader("123\r"); - Assert.AreEqual("123", lineReader.ReadLine().ToString()); - Assert.AreEqual(4, lineReader.SourcePosition); - Assert.Null(lineReader.ReadLine().Text); - } + [Test] + public void TestCr() + { + var lineReader = new LineReader("123\r"); + Assert.AreEqual("123", lineReader.ReadLine().ToString()); + Assert.AreEqual(4, lineReader.SourcePosition); + Assert.Null(lineReader.ReadLine().Text); + } - [Test] - public void TestCr2() - { - var lineReader = new LineReader("123\r456"); - Assert.AreEqual("123", lineReader.ReadLine().ToString()); - Assert.AreEqual(4, lineReader.SourcePosition); - Assert.AreEqual("456", lineReader.ReadLine().ToString()); - Assert.Null(lineReader.ReadLine().Text); - } + [Test] + public void TestCr2() + { + var lineReader = new LineReader("123\r456"); + Assert.AreEqual("123", lineReader.ReadLine().ToString()); + Assert.AreEqual(4, lineReader.SourcePosition); + Assert.AreEqual("456", lineReader.ReadLine().ToString()); + Assert.Null(lineReader.ReadLine().Text); + } - [Test] - public void TestCrLf() - { - // When limited == true, we limit the internal buffer exactly after the first new line char '\r' - // and we check that we don't get a new line for `\n` - var lineReader = new LineReader("123\r\n"); - Assert.AreEqual("123", lineReader.ReadLine().ToString()); - Assert.AreEqual(5, lineReader.SourcePosition); - Assert.Null(lineReader.ReadLine().Text); - } + [Test] + public void TestCrLf() + { + // When limited == true, we limit the internal buffer exactly after the first new line char '\r' + // and we check that we don't get a new line for `\n` + var lineReader = new LineReader("123\r\n"); + Assert.AreEqual("123", lineReader.ReadLine().ToString()); + Assert.AreEqual(5, lineReader.SourcePosition); + Assert.Null(lineReader.ReadLine().Text); + } - [Test] - public void TestCrLf2() - { - var lineReader = new LineReader("123\r\n456"); - Assert.AreEqual("123", lineReader.ReadLine().ToString()); - Assert.AreEqual(5, lineReader.SourcePosition); - Assert.AreEqual("456", lineReader.ReadLine().ToString()); - Assert.Null(lineReader.ReadLine().Text); - } + [Test] + public void TestCrLf2() + { + var lineReader = new LineReader("123\r\n456"); + Assert.AreEqual("123", lineReader.ReadLine().ToString()); + Assert.AreEqual(5, lineReader.SourcePosition); + Assert.AreEqual("456", lineReader.ReadLine().ToString()); + Assert.Null(lineReader.ReadLine().Text); } } \ No newline at end of file diff --git a/src/Markdig.Tests/TestLinkHelper.cs b/src/Markdig.Tests/TestLinkHelper.cs index 345dd3e54..33edb465e 100644 --- a/src/Markdig.Tests/TestLinkHelper.cs +++ b/src/Markdig.Tests/TestLinkHelper.cs @@ -2,374 +2,372 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using NUnit.Framework; using Markdig.Helpers; using Markdig.Syntax; -namespace Markdig.Tests +namespace Markdig.Tests; + +[TestFixture] +public class TestLinkHelper { - [TestFixture] - public class TestLinkHelper - { - [Test] - public void TestUrlSimple() - { - var text = new StringSlice("toto tutu"); - Assert.True(LinkHelper.TryParseUrl(ref text, out string link, out _)); - Assert.AreEqual("toto", link); - Assert.AreEqual(' ', text.CurrentChar); - } - - [Test] - public void TestUrlUrl() - { - var text = new StringSlice("http://google.com)"); - Assert.True(LinkHelper.TryParseUrl(ref text, out string link, out _)); - Assert.AreEqual("http://google.com", link); - Assert.AreEqual(')', text.CurrentChar); - } - - [Test] - [TestCase("http://google.com.")] - [TestCase("http://google.com. ")] - public void TestUrlTrailingFullStop(string uri) - { - var text = new StringSlice(uri); - Assert.True(LinkHelper.TryParseUrl(ref text, out string link, out _, true)); - Assert.AreEqual("http://google.com", link); - Assert.AreEqual('.', text.CurrentChar); - } - - [Test] - public void TestUrlNestedParenthesis() - { - var text = new StringSlice("(toto)tutu(tata) nooo"); - Assert.True(LinkHelper.TryParseUrl(ref text, out string link, out _)); - Assert.AreEqual("(toto)tutu(tata)", link); - Assert.AreEqual(' ', text.CurrentChar); - } - - [Test] - public void TestUrlAlternate() - { - var text = new StringSlice(" nooo"); - Assert.True(LinkHelper.TryParseUrl(ref text, out string link, out _)); - Assert.AreEqual("toto_tata_tutu", link); - Assert.AreEqual(' ', text.CurrentChar); - } - - [Test] - public void TestUrlAlternateInvalid() - { - var text = new StringSlice(")A"); - Assert.True(LinkHelper.TryParseInlineLink(ref text, out string link, out string title, out SourceSpan linkSpan, out SourceSpan titleSpan)); - Assert.AreEqual(string.Empty, link); - Assert.AreEqual(string.Empty, title); - Assert.AreEqual(new SourceSpan(1, 2), linkSpan); - Assert.AreEqual(SourceSpan.Empty, titleSpan); - Assert.AreEqual('A', text.CurrentChar); - } - - [Test] - public void TestUrlAndTitleEmpty2() - { - // 012345 - var text = new StringSlice(@"( <> )A"); - Assert.True(LinkHelper.TryParseInlineLink(ref text, out string link, out string title, out SourceSpan linkSpan, out SourceSpan titleSpan)); - Assert.AreEqual(string.Empty, link); - Assert.AreEqual(string.Empty, title); - Assert.AreEqual(new SourceSpan(2, 3), linkSpan); - Assert.AreEqual(SourceSpan.Empty, titleSpan); - Assert.AreEqual('A', text.CurrentChar); - } - - - [Test] - public void TestUrlEmptyWithTitleWithMultipleSpaces() - { - // 0 1 2 - // 0123456789012345678901234567 - var text = new StringSlice(@"( <> 'toto' )A"); - Assert.True(LinkHelper.TryParseInlineLink(ref text, out string link, out string title, out SourceSpan linkSpan, out SourceSpan titleSpan)); - Assert.AreEqual(string.Empty, link); - Assert.AreEqual("toto", title); - Assert.AreEqual(new SourceSpan(4, 5), linkSpan); - Assert.AreEqual(new SourceSpan(12, 17), titleSpan); - Assert.AreEqual('A', text.CurrentChar); - } - - [Test] - public void TestUrlEmpty() - { - var text = new StringSlice(@"()A"); - Assert.True(LinkHelper.TryParseInlineLink(ref text, out string link, out string title, out SourceSpan linkSpan, out SourceSpan titleSpan)); - Assert.AreEqual(string.Empty, link); - Assert.AreEqual(string.Empty, title); - Assert.AreEqual(SourceSpan.Empty, linkSpan); - Assert.AreEqual(SourceSpan.Empty, titleSpan); - Assert.AreEqual('A', text.CurrentChar); - } - - [Test] - public void TestMultipleLines() - { - // 0 1 2 3 - // 01 2345678901234567890 1234567890123456789 - var text = new StringSlice("(\n\n 'toto' )A"); - Assert.True(LinkHelper.TryParseInlineLink(ref text, out string link, out string title, out SourceSpan linkSpan, out SourceSpan titleSpan)); - Assert.AreEqual("http://google.com", link); - Assert.AreEqual("toto", title); - Assert.AreEqual(new SourceSpan(2, 20), linkSpan); - Assert.AreEqual(new SourceSpan(26, 31), titleSpan); - Assert.AreEqual('A', text.CurrentChar); - } - - [Test] - public void TestLabelSimple() - { - // 01234 - var text = new StringSlice("[foo]"); - Assert.True(LinkHelper.TryParseLabel(ref text, out string label, out SourceSpan labelSpan)); - Assert.AreEqual(new SourceSpan(1, 3), labelSpan); - Assert.AreEqual("foo", label); - } - - [Test] - public void TestLabelEscape() - { - // 012345678 - var text = new StringSlice(@"[fo\[\]o]"); - Assert.True(LinkHelper.TryParseLabel(ref text, out string label, out SourceSpan labelSpan)); - Assert.AreEqual(new SourceSpan(1, 7), labelSpan); - Assert.AreEqual(@"fo[]o", label); - } - - [Test] - public void TestLabelEscape2() - { - // 0123 - var text = new StringSlice(@"[\]]"); - Assert.True(LinkHelper.TryParseLabel(ref text, out string label, out SourceSpan labelSpan)); - Assert.AreEqual(new SourceSpan(1, 2), labelSpan); - Assert.AreEqual(@"]", label); - } - - [Test] - public void TestLabelInvalids() - { - Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"a"), out string label)); - Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"["), out label)); - Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"[\x]"), out label)); - Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"[[]"), out label)); - Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"[ ]"), out label)); - Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"[ \t \n ]"), out label)); - } - - [Test] - public void TestLabelWhitespaceCollapsedAndTrim() - { - // 0 1 2 3 - // 0123456789012345678901234567890123456789 - var text = new StringSlice(@"[ fo o z ]"); - Assert.True(LinkHelper.TryParseLabel(ref text, out string label, out SourceSpan labelSpan)); - Assert.AreEqual(new SourceSpan(6, 17), labelSpan); - Assert.AreEqual(@"fo o z", label); - } - - [Test] - public void TestlLinkReferenceDefinitionSimple() - { - // 0 1 2 3 - // 0123456789012345678901234567890123456789 - var text = new StringSlice(@"[foo]: /toto 'title'"); - Assert.True(LinkHelper.TryParseLinkReferenceDefinition(ref text, out string label, out string url, out string title, out SourceSpan labelSpan, out SourceSpan urlSpan, out SourceSpan titleSpan)); - Assert.AreEqual(@"foo", label); - Assert.AreEqual(@"/toto", url); - Assert.AreEqual(@"title", title); - Assert.AreEqual(new SourceSpan(1, 3), labelSpan); - Assert.AreEqual(new SourceSpan(7, 11), urlSpan); - Assert.AreEqual(new SourceSpan(13, 19), titleSpan); - - } - - [Test] - public void TestAutoLinkUrlSimple() - { - var text = new StringSlice(@""); - Assert.True(LinkHelper.TryParseAutolink(ref text, out string url, out bool isEmail)); - Assert.False(isEmail); - Assert.AreEqual("http://google.com", url); - } - - [Test] - public void TestAutoLinkEmailSimple() - { - var text = new StringSlice(@""); - Assert.True(LinkHelper.TryParseAutolink(ref text, out string email, out bool isEmail)); - Assert.True(isEmail); - Assert.AreEqual("user@host.com", email); - } - - [Test] - public void TestAutolinkInvalid() - { - Assert.False(LinkHelper.TryParseAutolink(new StringSlice(@""), out string text, out bool isEmail)); - Assert.False(LinkHelper.TryParseAutolink(new StringSlice(@"<"), out text, out isEmail)); - Assert.False(LinkHelper.TryParseAutolink(new StringSlice(@""), out text, out isEmail)); - } - - [TestCase("Header identifiers in HTML", "header-identifiers-in-html")] - [TestCase("* Dogs*?--in *my* house?", "dogs-in-my-house")] // Not Pandoc equivalent: dogs--in... - [TestCase("[HTML], [S5], or [RTF]?", "html-s5-or-rtf")] - [TestCase("3. Applications", "applications")] - [TestCase("33", "")] - public void TestUrilizeNonAscii_Pandoc(string input, string expectedResult) - { - Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, false)); - } - - [TestCase("Header identifiers in HTML", "header-identifiers-in-html")] - [TestCase("* Dogs*?--in *my* house?", "-dogs--in-my-house")] - [TestCase("[HTML], [S5], or [RTF]?", "html-s5-or-rtf")] - [TestCase("3. Applications", "3-applications")] - [TestCase("33", "33")] - public void TestUrilizeGfm(string input, string expectedResult) - { - Assert.AreEqual(expectedResult, LinkHelper.UrilizeAsGfm(input)); - } - - [TestCase("abc", "abc")] - [TestCase("a-c", "a-c")] - [TestCase("a c", "a-c")] - [TestCase("a_c", "a_c")] - [TestCase("a.c", "a.c")] - [TestCase("a,c", "ac")] - [TestCase("a--", "a")] // Not Pandoc-equivalent: a-- - [TestCase("a__", "a")] // Not Pandoc-equivalent: a__ - [TestCase("a..", "a")] // Not Pandoc-equivalent: a.. - [TestCase("a??", "a")] - [TestCase("a ", "a")] - [TestCase("a--d", "a-d")] - [TestCase("a__d", "a_d")] - [TestCase("a??d", "ad")] - [TestCase("a d", "a-d")] - [TestCase("a..d", "a.d")] - [TestCase("-bc", "bc")] - [TestCase("_bc", "bc")] - [TestCase(" bc", "bc")] - [TestCase("?bc", "bc")] - [TestCase(".bc", "bc")] - [TestCase("a-.-", "a")] // Not Pandoc equivalent: a-.- - public void TestUrilizeOnlyAscii_Simple(string input, string expectedResult) - { - Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, true)); - } - - [TestCase("bær", "br")] - [TestCase("bør", "br")] - [TestCase("bΘr", "br")] - [TestCase("四五", "")] - public void TestUrilizeOnlyAscii_NonAscii(string input, string expectedResult) - { - Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, true)); - } - - [TestCase("bár", "bar")] - [TestCase("àrrivé", "arrive")] - public void TestUrilizeOnlyAscii_Normalization(string input, string expectedResult) - { - Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, true)); - } - - [TestCase("123", "")] - [TestCase("1,-b", "b")] - [TestCase("b1,-", "b1")] // Not Pandoc equivalent: b1- - [TestCase("ab3", "ab3")] - [TestCase("ab3de", "ab3de")] - public void TestUrilizeOnlyAscii_Numeric(string input, string expectedResult) - { - Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, true)); - } - - [TestCase("一二三四五", "一二三四五")] - [TestCase("一,-b", "一-b")] - public void TestUrilizeNonAscii_NonAsciiNumeric(string input, string expectedResult) - { - Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, false)); - } - - [TestCase("bær", "bær")] - [TestCase("æ5el", "æ5el")] - [TestCase("-æ5el", "æ5el")] - [TestCase("-frø-", "frø")] - [TestCase("-fr-ø", "fr-ø")] - public void TestUrilizeNonAscii_Simple(string input, string expectedResult) - { - Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, false)); - } - - // Just to be sure, test for characters expressly forbidden in URI fragments: - [TestCase("b#r", "br")] - [TestCase("b%r", "br")] // Invalid except as an escape character - [TestCase("b^r", "br")] - [TestCase("b[r", "br")] - [TestCase("b]r", "br")] - [TestCase("b{r", "br")] - [TestCase("b}r", "br")] - [TestCase("br", "br")] - [TestCase(@"b\r", "br")] - [TestCase(@"b""r", "br")] - [TestCase(@"Requirement 😀", "requirement")] - public void TestUrilizeNonAscii_NonValidCharactersForFragments(string input, string expectedResult) - { - Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, false)); - } - - [Test] - public void TestUnicodeInDomainNameOfLinkReferenceDefinition() - { - TestParser.TestSpec("[Foo]\n\n[Foo]: http://ünicode.com", "

    Foo

    "); - } + [Test] + public void TestUrlSimple() + { + var text = new StringSlice("toto tutu"); + Assert.True(LinkHelper.TryParseUrl(ref text, out string link, out _)); + Assert.AreEqual("toto", link); + Assert.AreEqual(' ', text.CurrentChar); + } + + [Test] + public void TestUrlUrl() + { + var text = new StringSlice("http://google.com)"); + Assert.True(LinkHelper.TryParseUrl(ref text, out string link, out _)); + Assert.AreEqual("http://google.com", link); + Assert.AreEqual(')', text.CurrentChar); + } + + [Test] + [TestCase("http://google.com.")] + [TestCase("http://google.com. ")] + public void TestUrlTrailingFullStop(string uri) + { + var text = new StringSlice(uri); + Assert.True(LinkHelper.TryParseUrl(ref text, out string link, out _, true)); + Assert.AreEqual("http://google.com", link); + Assert.AreEqual('.', text.CurrentChar); + } + + [Test] + public void TestUrlNestedParenthesis() + { + var text = new StringSlice("(toto)tutu(tata) nooo"); + Assert.True(LinkHelper.TryParseUrl(ref text, out string link, out _)); + Assert.AreEqual("(toto)tutu(tata)", link); + Assert.AreEqual(' ', text.CurrentChar); + } + + [Test] + public void TestUrlAlternate() + { + var text = new StringSlice(" nooo"); + Assert.True(LinkHelper.TryParseUrl(ref text, out string link, out _)); + Assert.AreEqual("toto_tata_tutu", link); + Assert.AreEqual(' ', text.CurrentChar); + } + + [Test] + public void TestUrlAlternateInvalid() + { + var text = new StringSlice(")A"); + Assert.True(LinkHelper.TryParseInlineLink(ref text, out string link, out string title, out SourceSpan linkSpan, out SourceSpan titleSpan)); + Assert.AreEqual(string.Empty, link); + Assert.AreEqual(string.Empty, title); + Assert.AreEqual(new SourceSpan(1, 2), linkSpan); + Assert.AreEqual(SourceSpan.Empty, titleSpan); + Assert.AreEqual('A', text.CurrentChar); + } + + [Test] + public void TestUrlAndTitleEmpty2() + { + // 012345 + var text = new StringSlice(@"( <> )A"); + Assert.True(LinkHelper.TryParseInlineLink(ref text, out string link, out string title, out SourceSpan linkSpan, out SourceSpan titleSpan)); + Assert.AreEqual(string.Empty, link); + Assert.AreEqual(string.Empty, title); + Assert.AreEqual(new SourceSpan(2, 3), linkSpan); + Assert.AreEqual(SourceSpan.Empty, titleSpan); + Assert.AreEqual('A', text.CurrentChar); + } + + + [Test] + public void TestUrlEmptyWithTitleWithMultipleSpaces() + { + // 0 1 2 + // 0123456789012345678901234567 + var text = new StringSlice(@"( <> 'toto' )A"); + Assert.True(LinkHelper.TryParseInlineLink(ref text, out string link, out string title, out SourceSpan linkSpan, out SourceSpan titleSpan)); + Assert.AreEqual(string.Empty, link); + Assert.AreEqual("toto", title); + Assert.AreEqual(new SourceSpan(4, 5), linkSpan); + Assert.AreEqual(new SourceSpan(12, 17), titleSpan); + Assert.AreEqual('A', text.CurrentChar); + } + + [Test] + public void TestUrlEmpty() + { + var text = new StringSlice(@"()A"); + Assert.True(LinkHelper.TryParseInlineLink(ref text, out string link, out string title, out SourceSpan linkSpan, out SourceSpan titleSpan)); + Assert.AreEqual(string.Empty, link); + Assert.AreEqual(string.Empty, title); + Assert.AreEqual(SourceSpan.Empty, linkSpan); + Assert.AreEqual(SourceSpan.Empty, titleSpan); + Assert.AreEqual('A', text.CurrentChar); + } + + [Test] + public void TestMultipleLines() + { + // 0 1 2 3 + // 01 2345678901234567890 1234567890123456789 + var text = new StringSlice("(\n\n 'toto' )A"); + Assert.True(LinkHelper.TryParseInlineLink(ref text, out string link, out string title, out SourceSpan linkSpan, out SourceSpan titleSpan)); + Assert.AreEqual("http://google.com", link); + Assert.AreEqual("toto", title); + Assert.AreEqual(new SourceSpan(2, 20), linkSpan); + Assert.AreEqual(new SourceSpan(26, 31), titleSpan); + Assert.AreEqual('A', text.CurrentChar); + } + + [Test] + public void TestLabelSimple() + { + // 01234 + var text = new StringSlice("[foo]"); + Assert.True(LinkHelper.TryParseLabel(ref text, out string label, out SourceSpan labelSpan)); + Assert.AreEqual(new SourceSpan(1, 3), labelSpan); + Assert.AreEqual("foo", label); + } + + [Test] + public void TestLabelEscape() + { + // 012345678 + var text = new StringSlice(@"[fo\[\]o]"); + Assert.True(LinkHelper.TryParseLabel(ref text, out string label, out SourceSpan labelSpan)); + Assert.AreEqual(new SourceSpan(1, 7), labelSpan); + Assert.AreEqual(@"fo[]o", label); + } + + [Test] + public void TestLabelEscape2() + { + // 0123 + var text = new StringSlice(@"[\]]"); + Assert.True(LinkHelper.TryParseLabel(ref text, out string label, out SourceSpan labelSpan)); + Assert.AreEqual(new SourceSpan(1, 2), labelSpan); + Assert.AreEqual(@"]", label); + } + + [Test] + public void TestLabelInvalids() + { + Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"a"), out string label)); + Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"["), out label)); + Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"[\x]"), out label)); + Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"[[]"), out label)); + Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"[ ]"), out label)); + Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"[ \t \n ]"), out label)); + } + + [Test] + public void TestLabelWhitespaceCollapsedAndTrim() + { + // 0 1 2 3 + // 0123456789012345678901234567890123456789 + var text = new StringSlice(@"[ fo o z ]"); + Assert.True(LinkHelper.TryParseLabel(ref text, out string label, out SourceSpan labelSpan)); + Assert.AreEqual(new SourceSpan(6, 17), labelSpan); + Assert.AreEqual(@"fo o z", label); + } + + [Test] + public void TestlLinkReferenceDefinitionSimple() + { + // 0 1 2 3 + // 0123456789012345678901234567890123456789 + var text = new StringSlice(@"[foo]: /toto 'title'"); + Assert.True(LinkHelper.TryParseLinkReferenceDefinition(ref text, out string label, out string url, out string title, out SourceSpan labelSpan, out SourceSpan urlSpan, out SourceSpan titleSpan)); + Assert.AreEqual(@"foo", label); + Assert.AreEqual(@"/toto", url); + Assert.AreEqual(@"title", title); + Assert.AreEqual(new SourceSpan(1, 3), labelSpan); + Assert.AreEqual(new SourceSpan(7, 11), urlSpan); + Assert.AreEqual(new SourceSpan(13, 19), titleSpan); + + } + + [Test] + public void TestAutoLinkUrlSimple() + { + var text = new StringSlice(@""); + Assert.True(LinkHelper.TryParseAutolink(ref text, out string url, out bool isEmail)); + Assert.False(isEmail); + Assert.AreEqual("http://google.com", url); + } + + [Test] + public void TestAutoLinkEmailSimple() + { + var text = new StringSlice(@""); + Assert.True(LinkHelper.TryParseAutolink(ref text, out string email, out bool isEmail)); + Assert.True(isEmail); + Assert.AreEqual("user@host.com", email); + } + + [Test] + public void TestAutolinkInvalid() + { + Assert.False(LinkHelper.TryParseAutolink(new StringSlice(@""), out string text, out bool isEmail)); + Assert.False(LinkHelper.TryParseAutolink(new StringSlice(@"<"), out text, out isEmail)); + Assert.False(LinkHelper.TryParseAutolink(new StringSlice(@""), out text, out isEmail)); + } + + [TestCase("Header identifiers in HTML", "header-identifiers-in-html")] + [TestCase("* Dogs*?--in *my* house?", "dogs-in-my-house")] // Not Pandoc equivalent: dogs--in... + [TestCase("[HTML], [S5], or [RTF]?", "html-s5-or-rtf")] + [TestCase("3. Applications", "applications")] + [TestCase("33", "")] + public void TestUrilizeNonAscii_Pandoc(string input, string expectedResult) + { + Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, false)); + } + + [TestCase("Header identifiers in HTML", "header-identifiers-in-html")] + [TestCase("* Dogs*?--in *my* house?", "-dogs--in-my-house")] + [TestCase("[HTML], [S5], or [RTF]?", "html-s5-or-rtf")] + [TestCase("3. Applications", "3-applications")] + [TestCase("33", "33")] + public void TestUrilizeGfm(string input, string expectedResult) + { + Assert.AreEqual(expectedResult, LinkHelper.UrilizeAsGfm(input)); + } + + [TestCase("abc", "abc")] + [TestCase("a-c", "a-c")] + [TestCase("a c", "a-c")] + [TestCase("a_c", "a_c")] + [TestCase("a.c", "a.c")] + [TestCase("a,c", "ac")] + [TestCase("a--", "a")] // Not Pandoc-equivalent: a-- + [TestCase("a__", "a")] // Not Pandoc-equivalent: a__ + [TestCase("a..", "a")] // Not Pandoc-equivalent: a.. + [TestCase("a??", "a")] + [TestCase("a ", "a")] + [TestCase("a--d", "a-d")] + [TestCase("a__d", "a_d")] + [TestCase("a??d", "ad")] + [TestCase("a d", "a-d")] + [TestCase("a..d", "a.d")] + [TestCase("-bc", "bc")] + [TestCase("_bc", "bc")] + [TestCase(" bc", "bc")] + [TestCase("?bc", "bc")] + [TestCase(".bc", "bc")] + [TestCase("a-.-", "a")] // Not Pandoc equivalent: a-.- + public void TestUrilizeOnlyAscii_Simple(string input, string expectedResult) + { + Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, true)); + } + + [TestCase("bær", "br")] + [TestCase("bør", "br")] + [TestCase("bΘr", "br")] + [TestCase("四五", "")] + public void TestUrilizeOnlyAscii_NonAscii(string input, string expectedResult) + { + Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, true)); + } + + [TestCase("bár", "bar")] + [TestCase("àrrivé", "arrive")] + public void TestUrilizeOnlyAscii_Normalization(string input, string expectedResult) + { + Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, true)); + } + + [TestCase("123", "")] + [TestCase("1,-b", "b")] + [TestCase("b1,-", "b1")] // Not Pandoc equivalent: b1- + [TestCase("ab3", "ab3")] + [TestCase("ab3de", "ab3de")] + public void TestUrilizeOnlyAscii_Numeric(string input, string expectedResult) + { + Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, true)); + } + + [TestCase("一二三四五", "一二三四五")] + [TestCase("一,-b", "一-b")] + public void TestUrilizeNonAscii_NonAsciiNumeric(string input, string expectedResult) + { + Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, false)); + } + + [TestCase("bær", "bær")] + [TestCase("æ5el", "æ5el")] + [TestCase("-æ5el", "æ5el")] + [TestCase("-frø-", "frø")] + [TestCase("-fr-ø", "fr-ø")] + public void TestUrilizeNonAscii_Simple(string input, string expectedResult) + { + Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, false)); + } + + // Just to be sure, test for characters expressly forbidden in URI fragments: + [TestCase("b#r", "br")] + [TestCase("b%r", "br")] // Invalid except as an escape character + [TestCase("b^r", "br")] + [TestCase("b[r", "br")] + [TestCase("b]r", "br")] + [TestCase("b{r", "br")] + [TestCase("b}r", "br")] + [TestCase("br", "br")] + [TestCase(@"b\r", "br")] + [TestCase(@"b""r", "br")] + [TestCase(@"Requirement 😀", "requirement")] + public void TestUrilizeNonAscii_NonValidCharactersForFragments(string input, string expectedResult) + { + Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, false)); + } + + [Test] + public void TestUnicodeInDomainNameOfLinkReferenceDefinition() + { + TestParser.TestSpec("[Foo]\n\n[Foo]: http://ünicode.com", "

    Foo

    "); } } \ No newline at end of file diff --git a/src/Markdig.Tests/TestLinkRewriter.cs b/src/Markdig.Tests/TestLinkRewriter.cs index be1a84829..fa2ea1df4 100644 --- a/src/Markdig.Tests/TestLinkRewriter.cs +++ b/src/Markdig.Tests/TestLinkRewriter.cs @@ -1,44 +1,40 @@ -using System; -using System.IO; using Markdig.Parsers; using Markdig.Renderers; -using NUnit.Framework; -namespace Markdig.Tests +namespace Markdig.Tests; + +public class TestLinkRewriter { - public class TestLinkRewriter + [Test] + public void ReplacesRelativeLinks() { - [Test] - public void ReplacesRelativeLinks() - { - TestSpec(s => "abc" + s, "Link: [hello](/relative.jpg)", "abc/relative.jpg"); - TestSpec(s => s + "xyz", "Link: [hello](relative.jpg)", "relative.jpgxyz"); - TestSpec(null, "Link: [hello](relative.jpg)", "relative.jpg"); - TestSpec(null, "Link: [hello](/relative.jpg)", "/relative.jpg"); - } + TestSpec(s => "abc" + s, "Link: [hello](/relative.jpg)", "abc/relative.jpg"); + TestSpec(s => s + "xyz", "Link: [hello](relative.jpg)", "relative.jpgxyz"); + TestSpec(null, "Link: [hello](relative.jpg)", "relative.jpg"); + TestSpec(null, "Link: [hello](/relative.jpg)", "/relative.jpg"); + } - [Test] - public void ReplacesRelativeImageSources() - { - TestSpec(s => "abc" + s, "Image: ![alt text](/image.jpg)", "abc/image.jpg"); - TestSpec(s => "abc" + s, "Image: ![alt text](image.jpg \"title\")", "abcimage.jpg"); - TestSpec(null, "Image: ![alt text](/image.jpg)", "/image.jpg"); - } + [Test] + public void ReplacesRelativeImageSources() + { + TestSpec(s => "abc" + s, "Image: ![alt text](/image.jpg)", "abc/image.jpg"); + TestSpec(s => "abc" + s, "Image: ![alt text](image.jpg \"title\")", "abcimage.jpg"); + TestSpec(null, "Image: ![alt text](/image.jpg)", "/image.jpg"); + } - public static void TestSpec(Func linkRewriter, string markdown, string expectedLink) - { - var pipeline = new MarkdownPipelineBuilder().Build(); + public static void TestSpec(Func linkRewriter, string markdown, string expectedLink) + { + var pipeline = new MarkdownPipelineBuilder().Build(); - var writer = new StringWriter(); - var renderer = new HtmlRenderer(writer); - renderer.LinkRewriter = linkRewriter; - pipeline.Setup(renderer); + var writer = new StringWriter(); + var renderer = new HtmlRenderer(writer); + renderer.LinkRewriter = linkRewriter; + pipeline.Setup(renderer); - var document = MarkdownParser.Parse(markdown, pipeline); - renderer.Render(document); - writer.Flush(); + var document = MarkdownParser.Parse(markdown, pipeline); + renderer.Render(document); + writer.Flush(); - Assert.That(writer.ToString(), Contains.Substring("=\"" + expectedLink + "\"")); - } + Assert.That(writer.ToString(), Contains.Substring("=\"" + expectedLink + "\"")); } } \ No newline at end of file diff --git a/src/Markdig.Tests/TestMarkdigCoreApi.cs b/src/Markdig.Tests/TestMarkdigCoreApi.cs index 1f23c71e5..b201df9f5 100644 --- a/src/Markdig.Tests/TestMarkdigCoreApi.cs +++ b/src/Markdig.Tests/TestMarkdigCoreApi.cs @@ -1,217 +1,214 @@ using Markdig.Renderers; using Markdig.Syntax; using Markdig.Syntax.Inlines; -using NUnit.Framework; -using System.IO; -namespace Markdig.Tests +namespace Markdig.Tests; + +public class TestMarkdigCoreApi { - public class TestMarkdigCoreApi + [Test] + public void TestToHtml() { - [Test] - public void TestToHtml() + for (int i = 0; i < 5; i++) { - for (int i = 0; i < 5; i++) - { - string html = Markdown.ToHtml("This is a text with some *emphasis*"); - Assert.AreEqual("

    This is a text with some emphasis

    \n", html); - - html = Markdown.ToHtml("This is a text with a https://link.tld/"); - Assert.AreNotEqual("

    This is a text with a https://link.tld/

    \n", html); - } + string html = Markdown.ToHtml("This is a text with some *emphasis*"); + Assert.AreEqual("

    This is a text with some emphasis

    \n", html); + + html = Markdown.ToHtml("This is a text with a https://link.tld/"); + Assert.AreNotEqual("

    This is a text with a https://link.tld/

    \n", html); } + } - [Test] - public void TestToHtmlWithPipeline() + [Test] + public void TestToHtmlWithPipeline() + { + var pipeline = new MarkdownPipelineBuilder() + .Build(); + + for (int i = 0; i < 5; i++) { - var pipeline = new MarkdownPipelineBuilder() - .Build(); - - for (int i = 0; i < 5; i++) - { - string html = Markdown.ToHtml("This is a text with some *emphasis*", pipeline); - Assert.AreEqual("

    This is a text with some emphasis

    \n", html); - - html = Markdown.ToHtml("This is a text with a https://link.tld/", pipeline); - Assert.AreNotEqual("

    This is a text with a https://link.tld/

    \n", html); - } - - pipeline = new MarkdownPipelineBuilder() - .UseAdvancedExtensions() - .Build(); - - for (int i = 0; i < 5; i++) - { - string html = Markdown.ToHtml("This is a text with a https://link.tld/", pipeline); - Assert.AreEqual("

    This is a text with a https://link.tld/

    \n", html); - } + string html = Markdown.ToHtml("This is a text with some *emphasis*", pipeline); + Assert.AreEqual("

    This is a text with some emphasis

    \n", html); + + html = Markdown.ToHtml("This is a text with a https://link.tld/", pipeline); + Assert.AreNotEqual("

    This is a text with a https://link.tld/

    \n", html); } - [Test] - public void TestToHtmlWithWriter() + pipeline = new MarkdownPipelineBuilder() + .UseAdvancedExtensions() + .Build(); + + for (int i = 0; i < 5; i++) { - var writer = new StringWriter(); + string html = Markdown.ToHtml("This is a text with a https://link.tld/", pipeline); + Assert.AreEqual("

    This is a text with a https://link.tld/

    \n", html); + } + } + + [Test] + public void TestToHtmlWithWriter() + { + var writer = new StringWriter(); - for (int i = 0; i < 5; i++) - { - _ = Markdown.ToHtml("This is a text with some *emphasis*", writer); - string html = writer.ToString(); - Assert.AreEqual("

    This is a text with some emphasis

    \n", html); - writer.GetStringBuilder().Length = 0; - } - - writer = new StringWriter(); - var pipeline = new MarkdownPipelineBuilder() - .UseAdvancedExtensions() - .Build(); - - for (int i = 0; i < 5; i++) - { - _ = Markdown.ToHtml("This is a text with a https://link.tld/", writer, pipeline); - string html = writer.ToString(); - Assert.AreEqual("

    This is a text with a https://link.tld/

    \n", html); - writer.GetStringBuilder().Length = 0; - } + for (int i = 0; i < 5; i++) + { + _ = Markdown.ToHtml("This is a text with some *emphasis*", writer); + string html = writer.ToString(); + Assert.AreEqual("

    This is a text with some emphasis

    \n", html); + writer.GetStringBuilder().Length = 0; } - [Test] - public void TestDocumentToHtmlWithWriter() + writer = new StringWriter(); + var pipeline = new MarkdownPipelineBuilder() + .UseAdvancedExtensions() + .Build(); + + for (int i = 0; i < 5; i++) { - var writer = new StringWriter(); + _ = Markdown.ToHtml("This is a text with a https://link.tld/", writer, pipeline); + string html = writer.ToString(); + Assert.AreEqual("

    This is a text with a https://link.tld/

    \n", html); + writer.GetStringBuilder().Length = 0; + } + } + + [Test] + public void TestDocumentToHtmlWithWriter() + { + var writer = new StringWriter(); - for (int i = 0; i < 5; i++) - { - MarkdownDocument document = Markdown.Parse("This is a text with some *emphasis*"); - document.ToHtml(writer); - string html = writer.ToString(); - Assert.AreEqual("

    This is a text with some emphasis

    \n", html); - writer.GetStringBuilder().Length = 0; - } - - writer = new StringWriter(); - var pipeline = new MarkdownPipelineBuilder() - .UseAdvancedExtensions() - .Build(); - - for (int i = 0; i < 5; i++) - { - MarkdownDocument document = Markdown.Parse("This is a text with a https://link.tld/", pipeline); - document.ToHtml(writer, pipeline); - string html = writer.ToString(); - Assert.AreEqual("

    This is a text with a https://link.tld/

    \n", html); - writer.GetStringBuilder().Length = 0; - } + for (int i = 0; i < 5; i++) + { + MarkdownDocument document = Markdown.Parse("This is a text with some *emphasis*"); + document.ToHtml(writer); + string html = writer.ToString(); + Assert.AreEqual("

    This is a text with some emphasis

    \n", html); + writer.GetStringBuilder().Length = 0; } - [Test] - public void TestConvert() + writer = new StringWriter(); + var pipeline = new MarkdownPipelineBuilder() + .UseAdvancedExtensions() + .Build(); + + for (int i = 0; i < 5; i++) { - var writer = new StringWriter(); - var renderer = new HtmlRenderer(writer); - - for (int i = 0; i < 5; i++) - { - _ = Markdown.Convert("This is a text with some *emphasis*", renderer); - string html = writer.ToString(); - Assert.AreEqual("

    This is a text with some emphasis

    \n", html); - writer.GetStringBuilder().Length = 0; - } - - writer = new StringWriter(); - renderer = new HtmlRenderer(writer); - var pipeline = new MarkdownPipelineBuilder() - .UseAdvancedExtensions() - .Build(); - - for (int i = 0; i < 5; i++) - { - _ = Markdown.Convert("This is a text with a https://link.tld/", renderer, pipeline); - string html = writer.ToString(); - Assert.AreEqual("

    This is a text with a https://link.tld/

    \n", html); - writer.GetStringBuilder().Length = 0; - } + MarkdownDocument document = Markdown.Parse("This is a text with a https://link.tld/", pipeline); + document.ToHtml(writer, pipeline); + string html = writer.ToString(); + Assert.AreEqual("

    This is a text with a https://link.tld/

    \n", html); + writer.GetStringBuilder().Length = 0; + } + } + + [Test] + public void TestConvert() + { + var writer = new StringWriter(); + var renderer = new HtmlRenderer(writer); + + for (int i = 0; i < 5; i++) + { + _ = Markdown.Convert("This is a text with some *emphasis*", renderer); + string html = writer.ToString(); + Assert.AreEqual("

    This is a text with some emphasis

    \n", html); + writer.GetStringBuilder().Length = 0; + } + + writer = new StringWriter(); + renderer = new HtmlRenderer(writer); + var pipeline = new MarkdownPipelineBuilder() + .UseAdvancedExtensions() + .Build(); + + for (int i = 0; i < 5; i++) + { + _ = Markdown.Convert("This is a text with a https://link.tld/", renderer, pipeline); + string html = writer.ToString(); + Assert.AreEqual("

    This is a text with a https://link.tld/

    \n", html); + writer.GetStringBuilder().Length = 0; } + } + + [Test] + public void TestParse() + { + const string markdown = "This is a text with some *emphasis*"; + + var pipeline = new MarkdownPipelineBuilder() + .UsePreciseSourceLocation() + .Build(); - [Test] - public void TestParse() + for (int i = 0; i < 5; i++) { - const string markdown = "This is a text with some *emphasis*"; - - var pipeline = new MarkdownPipelineBuilder() - .UsePreciseSourceLocation() - .Build(); - - for (int i = 0; i < 5; i++) - { - MarkdownDocument document = Markdown.Parse(markdown, pipeline); - - Assert.AreEqual(1, document.LineCount); - Assert.AreEqual(markdown.Length, document.Span.Length); - Assert.AreEqual(1, document.LineStartIndexes.Count); - Assert.AreEqual(0, document.LineStartIndexes[0]); - - Assert.AreEqual(1, document.Count); - ParagraphBlock paragraph = document[0] as ParagraphBlock; - Assert.NotNull(paragraph); - Assert.AreEqual(markdown.Length, paragraph.Span.Length); - LiteralInline literal = paragraph.Inline.FirstChild as LiteralInline; - Assert.NotNull(literal); - Assert.AreEqual("This is a text with some ", literal.ToString()); - EmphasisInline emphasis = literal.NextSibling as EmphasisInline; - Assert.NotNull(emphasis); - Assert.AreEqual("*emphasis*".Length, emphasis.Span.Length); - LiteralInline emphasisLiteral = emphasis.FirstChild as LiteralInline; - Assert.NotNull(emphasisLiteral); - Assert.AreEqual("emphasis", emphasisLiteral.ToString()); - Assert.Null(emphasisLiteral.NextSibling); - Assert.Null(emphasis.NextSibling); - } + MarkdownDocument document = Markdown.Parse(markdown, pipeline); + + Assert.AreEqual(1, document.LineCount); + Assert.AreEqual(markdown.Length, document.Span.Length); + Assert.AreEqual(1, document.LineStartIndexes.Count); + Assert.AreEqual(0, document.LineStartIndexes[0]); + + Assert.AreEqual(1, document.Count); + ParagraphBlock paragraph = document[0] as ParagraphBlock; + Assert.NotNull(paragraph); + Assert.AreEqual(markdown.Length, paragraph.Span.Length); + LiteralInline literal = paragraph.Inline.FirstChild as LiteralInline; + Assert.NotNull(literal); + Assert.AreEqual("This is a text with some ", literal.ToString()); + EmphasisInline emphasis = literal.NextSibling as EmphasisInline; + Assert.NotNull(emphasis); + Assert.AreEqual("*emphasis*".Length, emphasis.Span.Length); + LiteralInline emphasisLiteral = emphasis.FirstChild as LiteralInline; + Assert.NotNull(emphasisLiteral); + Assert.AreEqual("emphasis", emphasisLiteral.ToString()); + Assert.Null(emphasisLiteral.NextSibling); + Assert.Null(emphasis.NextSibling); } + } - [Test] - public void TestNormalize() + [Test] + public void TestNormalize() + { + for (int i = 0; i < 5; i++) { - for (int i = 0; i < 5; i++) - { - string normalized = Markdown.Normalize("Heading\n======="); - Assert.AreEqual("# Heading", normalized); - } + string normalized = Markdown.Normalize("Heading\n======="); + Assert.AreEqual("# Heading", normalized); } + } - [Test] - public void TestNormalizeWithWriter() + [Test] + public void TestNormalizeWithWriter() + { + for (int i = 0; i < 5; i++) { - for (int i = 0; i < 5; i++) - { - var writer = new StringWriter(); - - _ = Markdown.Normalize("Heading\n=======", writer); - string normalized = writer.ToString(); - Assert.AreEqual("# Heading", normalized); - } + var writer = new StringWriter(); + + _ = Markdown.Normalize("Heading\n=======", writer); + string normalized = writer.ToString(); + Assert.AreEqual("# Heading", normalized); } + } - [Test] - public void TestToPlainText() + [Test] + public void TestToPlainText() + { + for (int i = 0; i < 5; i++) { - for (int i = 0; i < 5; i++) - { - string plainText = Markdown.ToPlainText("*Hello*, [world](http://example.com)!"); - Assert.AreEqual("Hello, world!\n", plainText); - } + string plainText = Markdown.ToPlainText("*Hello*, [world](http://example.com)!"); + Assert.AreEqual("Hello, world!\n", plainText); } + } - [Test] - public void TestToPlainTextWithWriter() + [Test] + public void TestToPlainTextWithWriter() + { + for (int i = 0; i < 5; i++) { - for (int i = 0; i < 5; i++) - { - var writer = new StringWriter(); - - _ = Markdown.ToPlainText("*Hello*, [world](http://example.com)!", writer); - string plainText = writer.ToString(); - Assert.AreEqual("Hello, world!\n", plainText); - } + var writer = new StringWriter(); + + _ = Markdown.ToPlainText("*Hello*, [world](http://example.com)!", writer); + string plainText = writer.ToString(); + Assert.AreEqual("Hello, world!\n", plainText); } } } diff --git a/src/Markdig.Tests/TestMediaLinks.cs b/src/Markdig.Tests/TestMediaLinks.cs index 27ba34262..64c5d3e98 100644 --- a/src/Markdig.Tests/TestMediaLinks.cs +++ b/src/Markdig.Tests/TestMediaLinks.cs @@ -1,104 +1,102 @@ -using Markdig.Extensions.MediaLinks; -using NUnit.Framework; -using System; using System.Text.RegularExpressions; -namespace Markdig.Tests -{ - [TestFixture] - public class TestMediaLinks - { - private MarkdownPipeline GetPipeline(MediaOptions options = null) - { - return new MarkdownPipelineBuilder() - .UseMediaLinks(options) - .Build(); - } +using Markdig.Extensions.MediaLinks; - private MarkdownPipeline GetPipelineWithBootstrap(MediaOptions options = null) - { - return new MarkdownPipelineBuilder() - .UseBootstrap() - .UseMediaLinks(options) - .Build(); - } +namespace Markdig.Tests; - [Test] - [TestCase("![static mp4](https://sample.com/video.mp4)", "

    \n")] - [TestCase("![static mp4](//sample.com/video.mp4)", "

    \n")] - [TestCase(@"![youtube.com](https://www.youtube.com/watch?v=mswPy5bt3TQ)", "

    \n")] - [TestCase("![yandex.ru](https://music.yandex.ru/album/411845/track/4402274)", "

    \n")] - [TestCase("![vimeo](https://vimeo.com/8607834)", "

    \n")] - [TestCase("![ok.ru](https://ok.ru/video/26870090463)", "

    \n")] - [TestCase("![ok.ru](//ok.ru/video/26870090463)", "

    \n")] - public void TestBuiltInHosts(string markdown, string expected) - { - string html = Markdown.ToHtml(markdown, GetPipeline()); - Assert.AreEqual(html, expected); - } +[TestFixture] +public class TestMediaLinks +{ + private MarkdownPipeline GetPipeline(MediaOptions options = null) + { + return new MarkdownPipelineBuilder() + .UseMediaLinks(options) + .Build(); + } - private class TestHostProvider : IHostProvider - { - public string Class { get; } = "regex"; - public bool AllowFullScreen { get; } + private MarkdownPipeline GetPipelineWithBootstrap(MediaOptions options = null) + { + return new MarkdownPipelineBuilder() + .UseBootstrap() + .UseMediaLinks(options) + .Build(); + } - public bool TryHandle(Uri mediaUri, bool isSchemaRelative, out string iframeUrl) - { - iframeUrl = null; - var uri = isSchemaRelative ? "//" + mediaUri.GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Scheme, UriFormat.UriEscaped) : mediaUri.ToString(); - if (!matcher.IsMatch(uri)) - return false; - iframeUrl = matcher.Replace(uri, replacement); - return true; - } + [Test] + [TestCase("![static mp4](https://sample.com/video.mp4)", "

    \n")] + [TestCase("![static mp4](//sample.com/video.mp4)", "

    \n")] + [TestCase(@"![youtube.com](https://www.youtube.com/watch?v=mswPy5bt3TQ)", "

    \n")] + [TestCase("![yandex.ru](https://music.yandex.ru/album/411845/track/4402274)", "

    \n")] + [TestCase("![vimeo](https://vimeo.com/8607834)", "

    \n")] + [TestCase("![ok.ru](https://ok.ru/video/26870090463)", "

    \n")] + [TestCase("![ok.ru](//ok.ru/video/26870090463)", "

    \n")] + public void TestBuiltInHosts(string markdown, string expected) + { + string html = Markdown.ToHtml(markdown, GetPipeline()); + Assert.AreEqual(html, expected); + } - private Regex matcher; - private string replacement; + private class TestHostProvider : IHostProvider + { + public string Class { get; } = "regex"; + public bool AllowFullScreen { get; } - public TestHostProvider(string provider, string replace) - { - matcher = new Regex(provider); - replacement = replace; - } + public bool TryHandle(Uri mediaUri, bool isSchemaRelative, out string iframeUrl) + { + iframeUrl = null; + var uri = isSchemaRelative ? "//" + mediaUri.GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Scheme, UriFormat.UriEscaped) : mediaUri.ToString(); + if (!matcher.IsMatch(uri)) + return false; + iframeUrl = matcher.Replace(uri, replacement); + return true; } - [Test] - [TestCase("![p1](https://sample.com/video.mp4)", "

    \n", @"^https?://sample.com/(.+)$", @"https://example.com/$1")] - [TestCase("![p1](//sample.com/video.mp4)", "

    \n", @"^//sample.com/(.+)$", @"https://example.com/$1")] - [TestCase("![p1](https://sample.com/video.mp4)", "

    \n", @"^https?://sample.com/(.+)$", @"https://example.com/$1?token=aaabbb")] - public void TestCustomHostProvider(string markdown, string expected, string provider, string replace) + private Regex matcher; + private string replacement; + + public TestHostProvider(string provider, string replace) { - string html = Markdown.ToHtml(markdown, GetPipeline(new MediaOptions - { - Hosts = - { - new TestHostProvider(provider, replace), - } - })); - Assert.AreEqual(html, expected); + matcher = new Regex(provider); + replacement = replace; } + } - [Test] - [TestCase("![static mp4](//sample.com/video.mp4)", "

    \n", "")] - [TestCase(@"![youtube.com](https://www.youtube.com/watch?v=mswPy5bt3TQ)", "

    \n", "")] - [TestCase("![static mp4](//sample.com/video.mp4)", "

    \n", "k")] - [TestCase(@"![youtube.com](https://www.youtube.com/watch?v=mswPy5bt3TQ)", "

    \n", "k")] - public void TestCustomClass(string markdown, string expected, string klass) + [Test] + [TestCase("![p1](https://sample.com/video.mp4)", "

    \n", @"^https?://sample.com/(.+)$", @"https://example.com/$1")] + [TestCase("![p1](//sample.com/video.mp4)", "

    \n", @"^//sample.com/(.+)$", @"https://example.com/$1")] + [TestCase("![p1](https://sample.com/video.mp4)", "

    \n", @"^https?://sample.com/(.+)$", @"https://example.com/$1?token=aaabbb")] + public void TestCustomHostProvider(string markdown, string expected, string provider, string replace) + { + string html = Markdown.ToHtml(markdown, GetPipeline(new MediaOptions { - string html = Markdown.ToHtml(markdown, GetPipeline(new MediaOptions + Hosts = { - Class = klass, - })); - Assert.AreEqual(html, expected); - } + new TestHostProvider(provider, replace), + } + })); + Assert.AreEqual(html, expected); + } - [Test] - [TestCase("![static mp4](//sample.com/video.mp4)", "

    \n")] - [TestCase(@"![youtube.com](https://www.youtube.com/watch?v=mswPy5bt3TQ)", "

    \n")] - public void TestWithBootstrap(string markdown, string expected) + [Test] + [TestCase("![static mp4](//sample.com/video.mp4)", "

    \n", "")] + [TestCase(@"![youtube.com](https://www.youtube.com/watch?v=mswPy5bt3TQ)", "

    \n", "")] + [TestCase("![static mp4](//sample.com/video.mp4)", "

    \n", "k")] + [TestCase(@"![youtube.com](https://www.youtube.com/watch?v=mswPy5bt3TQ)", "

    \n", "k")] + public void TestCustomClass(string markdown, string expected, string klass) + { + string html = Markdown.ToHtml(markdown, GetPipeline(new MediaOptions { - string html = Markdown.ToHtml(markdown, GetPipelineWithBootstrap()); - Assert.AreEqual(html, expected); - } + Class = klass, + })); + Assert.AreEqual(html, expected); + } + + [Test] + [TestCase("![static mp4](//sample.com/video.mp4)", "

    \n")] + [TestCase(@"![youtube.com](https://www.youtube.com/watch?v=mswPy5bt3TQ)", "

    \n")] + public void TestWithBootstrap(string markdown, string expected) + { + string html = Markdown.ToHtml(markdown, GetPipelineWithBootstrap()); + Assert.AreEqual(html, expected); } } diff --git a/src/Markdig.Tests/TestNewLine.cs b/src/Markdig.Tests/TestNewLine.cs index d30d29a99..49bb335cf 100644 --- a/src/Markdig.Tests/TestNewLine.cs +++ b/src/Markdig.Tests/TestNewLine.cs @@ -1,31 +1,28 @@ -using System.Linq; using Markdig.Syntax; using Markdig.Syntax.Inlines; -using NUnit.Framework; -namespace Markdig.Tests +namespace Markdig.Tests; + +[TestFixture] +public class TestNewLine { - [TestFixture] - public class TestNewLine + [TestCase("a \nb", "

    a
    \nb

    \n")] + [TestCase("a\\\nb", "

    a
    \nb

    \n")] + [TestCase("a `b\nc`", "

    a b c

    \n")] + [TestCase("# Text A\nText B\n\n## Text C", "

    Text A

    \n

    Text B

    \n

    Text C

    \n")] + public void Test(string value, string expectedHtml) { - [TestCase("a \nb", "

    a
    \nb

    \n")] - [TestCase("a\\\nb", "

    a
    \nb

    \n")] - [TestCase("a `b\nc`", "

    a b c

    \n")] - [TestCase("# Text A\nText B\n\n## Text C", "

    Text A

    \n

    Text B

    \n

    Text C

    \n")] - public void Test(string value, string expectedHtml) - { - Assert.AreEqual(expectedHtml, Markdown.ToHtml(value)); - Assert.AreEqual(expectedHtml, Markdown.ToHtml(value.Replace("\n", "\r\n"))); - } + Assert.AreEqual(expectedHtml, Markdown.ToHtml(value)); + Assert.AreEqual(expectedHtml, Markdown.ToHtml(value.Replace("\n", "\r\n"))); + } - [Test()] - public void TestEscapeLineBreak() - { - var input = "test\\\r\ntest1\r\n"; - var doc = Markdown.Parse(input); - var inlines = doc.Descendants().ToList(); - Assert.AreEqual(1, inlines.Count, "Invalid number of LineBreakInline"); - Assert.True(inlines[0].IsBackslash); - } + [Test()] + public void TestEscapeLineBreak() + { + var input = "test\\\r\ntest1\r\n"; + var doc = Markdown.Parse(input); + var inlines = doc.Descendants().ToList(); + Assert.AreEqual(1, inlines.Count, "Invalid number of LineBreakInline"); + Assert.True(inlines[0].IsBackslash); } } diff --git a/src/Markdig.Tests/TestNormalize.cs b/src/Markdig.Tests/TestNormalize.cs index b3c0b573c..adef0691d 100644 --- a/src/Markdig.Tests/TestNormalize.cs +++ b/src/Markdig.Tests/TestNormalize.cs @@ -2,180 +2,177 @@ // 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 NUnit.Framework; +using Markdig.Helpers; +using Markdig.Renderers.Normalize; using Markdig.Syntax; using Markdig.Syntax.Inlines; -using System.IO; -using Markdig.Renderers.Normalize; -using Markdig.Helpers; -namespace Markdig.Tests +namespace Markdig.Tests; + +[TestFixture] +public class TestNormalize { - [TestFixture] - public class TestNormalize + [Test] + public void SyntaxCodeBlock() { - [Test] - public void SyntaxCodeBlock() + AssertSyntax("````csharp\npublic void HelloWorld()\n{\n}\n````", new FencedCodeBlock(null) { - AssertSyntax("````csharp\npublic void HelloWorld()\n{\n}\n````", new FencedCodeBlock(null) - { - FencedChar = '`', - OpeningFencedCharCount = 4, - ClosingFencedCharCount = 4, - Info = "csharp", - Lines = new StringLineGroup(4) - { - new StringSlice("public void HelloWorld()"), - new StringSlice("{"), - new StringSlice("}"), - } - }); - - AssertSyntax(" public void HelloWorld()\n {\n }", new CodeBlock(null) + FencedChar = '`', + OpeningFencedCharCount = 4, + ClosingFencedCharCount = 4, + Info = "csharp", + Lines = new StringLineGroup(4) { - Lines = new StringLineGroup(4) - { - new StringSlice("public void HelloWorld()"), - new StringSlice("{"), - new StringSlice("}"), - } - }); - } + new StringSlice("public void HelloWorld()"), + new StringSlice("{"), + new StringSlice("}"), + } + }); - [Test] - public void SyntaxHeadline() + AssertSyntax(" public void HelloWorld()\n {\n }", new CodeBlock(null) { - AssertSyntax("## Headline", new HeadingBlock(null) + Lines = new StringLineGroup(4) { - HeaderChar = '#', - Level = 2, - Inline = new ContainerInline().AppendChild(new LiteralInline("Headline")), - }); - } + new StringSlice("public void HelloWorld()"), + new StringSlice("{"), + new StringSlice("}"), + } + }); + } - [Test] - public void SyntaxParagraph() + [Test] + public void SyntaxHeadline() + { + AssertSyntax("## Headline", new HeadingBlock(null) { - AssertSyntax("This is a normal paragraph", new ParagraphBlock() - { - Inline = new ContainerInline() - .AppendChild(new LiteralInline("This is a normal paragraph")), - }); - - AssertSyntax("This is a\nnormal\nparagraph", new ParagraphBlock() - { - Inline = new ContainerInline() - .AppendChild(new LiteralInline("This is a")) - .AppendChild(new LineBreakInline()) - .AppendChild(new LiteralInline("normal")) - .AppendChild(new LineBreakInline()) - .AppendChild(new LiteralInline("paragraph")), - }); - } + HeaderChar = '#', + Level = 2, + Inline = new ContainerInline().AppendChild(new LiteralInline("Headline")), + }); + } - [Test] - public void CodeBlock() + [Test] + public void SyntaxParagraph() + { + AssertSyntax("This is a normal paragraph", new ParagraphBlock() { - AssertNormalizeNoTrim(" public void HelloWorld();\n {\n }"); - AssertNormalizeNoTrim(" public void HelloWorld();\n {\n }\n\ntext after two newlines"); - AssertNormalizeNoTrim("````\npublic void HelloWorld();\n{\n}\n````\n\ntext after two newlines"); - AssertNormalizeNoTrim("````csharp\npublic void HelloWorld();\n{\n}\n````"); - AssertNormalizeNoTrim("````csharp hideNewKeyword=true\npublic void HelloWorld();\n{\n}\n````"); - } + Inline = new ContainerInline() + .AppendChild(new LiteralInline("This is a normal paragraph")), + }); - [Test] - public void Heading() + AssertSyntax("This is a\nnormal\nparagraph", new ParagraphBlock() { - AssertNormalizeNoTrim("# Heading"); - AssertNormalizeNoTrim("## Heading"); - AssertNormalizeNoTrim("### Heading"); - AssertNormalizeNoTrim("#### Heading"); - AssertNormalizeNoTrim("##### Heading"); - AssertNormalizeNoTrim("###### Heading"); - AssertNormalizeNoTrim("###### Heading\n\ntext after two newlines"); - AssertNormalizeNoTrim("# Heading\nAnd Text1\n\nAndText2", options: new NormalizeOptions() { EmptyLineAfterHeading = false }); - - AssertNormalizeNoTrim("Heading\n=======\n\ntext after two newlines", "# Heading\n\ntext after two newlines"); - } + Inline = new ContainerInline() + .AppendChild(new LiteralInline("This is a")) + .AppendChild(new LineBreakInline()) + .AppendChild(new LiteralInline("normal")) + .AppendChild(new LineBreakInline()) + .AppendChild(new LiteralInline("paragraph")), + }); + } - [Test] - public void Backslash() - { - AssertNormalizeNoTrim("This is a hardline \nAnd this is another hardline\\\nThis is standard newline"); - AssertNormalizeNoTrim("This is a line\nWith another line\nAnd a last line"); - } + [Test] + public void CodeBlock() + { + AssertNormalizeNoTrim(" public void HelloWorld();\n {\n }"); + AssertNormalizeNoTrim(" public void HelloWorld();\n {\n }\n\ntext after two newlines"); + AssertNormalizeNoTrim("````\npublic void HelloWorld();\n{\n}\n````\n\ntext after two newlines"); + AssertNormalizeNoTrim("````csharp\npublic void HelloWorld();\n{\n}\n````"); + AssertNormalizeNoTrim("````csharp hideNewKeyword=true\npublic void HelloWorld();\n{\n}\n````"); + } - [Test] - public void HtmlBlock() - { - /*AssertNormalizeNoTrim(@"
    + [Test] + public void Heading() + { + AssertNormalizeNoTrim("# Heading"); + AssertNormalizeNoTrim("## Heading"); + AssertNormalizeNoTrim("### Heading"); + AssertNormalizeNoTrim("#### Heading"); + AssertNormalizeNoTrim("##### Heading"); + AssertNormalizeNoTrim("###### Heading"); + AssertNormalizeNoTrim("###### Heading\n\ntext after two newlines"); + AssertNormalizeNoTrim("# Heading\nAnd Text1\n\nAndText2", options: new NormalizeOptions() { EmptyLineAfterHeading = false }); + + AssertNormalizeNoTrim("Heading\n=======\n\ntext after two newlines", "# Heading\n\ntext after two newlines"); + } + + [Test] + public void Backslash() + { + AssertNormalizeNoTrim("This is a hardline \nAnd this is another hardline\\\nThis is standard newline"); + AssertNormalizeNoTrim("This is a line\nWith another line\nAnd a last line"); + } + + [Test] + public void HtmlBlock() + { + /*AssertNormalizeNoTrim(@"
    ");*/ // TODO: Bug: Throws Exception during emit - } + } - [Test] - public void Paragraph() - { - AssertNormalizeNoTrim("This is a plain paragraph"); - AssertNormalizeNoTrim(@"This + [Test] + public void Paragraph() + { + AssertNormalizeNoTrim("This is a plain paragraph"); + AssertNormalizeNoTrim(@"This is a plain paragraph"); - } + } - [Test] - public void ParagraphMulti() - { - AssertNormalizeNoTrim(@"line1 + [Test] + public void ParagraphMulti() + { + AssertNormalizeNoTrim(@"line1 line2 line3"); - } + } - [Test] - public void ListUnordered() - { - AssertNormalizeNoTrim(@"- a + [Test] + public void ListUnordered() + { + AssertNormalizeNoTrim(@"- a - b - c"); - } + } - [Test] - public void ListUnorderedLoose() - { - AssertNormalizeNoTrim(@"- a + [Test] + public void ListUnorderedLoose() + { + AssertNormalizeNoTrim(@"- a - b - c"); - } + } - [Test] - public void ListOrderedLooseAndCodeBlock() - { - AssertNormalizeNoTrim(@"1. ``` + [Test] + public void ListOrderedLooseAndCodeBlock() + { + AssertNormalizeNoTrim(@"1. ``` foo ``` bar"); - } + } - [Test, Ignore("Not sure this is the correct normalize for this one. Need to check the specs")] - public void ListUnorderedLooseTop() - { - AssertNormalizeNoTrim(@"* foo + [Test, Ignore("Not sure this is the correct normalize for this one. Need to check the specs")] + public void ListUnorderedLooseTop() + { + AssertNormalizeNoTrim(@"* foo * bar baz", options: new NormalizeOptions() { ListItemCharacter = '*' }); - } + } - [Test] - public void ListUnorderedLooseMultiParagraph() - { - AssertNormalizeNoTrim( + [Test] + public void ListUnorderedLooseMultiParagraph() + { + AssertNormalizeNoTrim( @"- a And another paragraph a @@ -185,22 +182,22 @@ And another paragraph a And another paragraph b - c"); - } + } - [Test] - public void ListOrdered() - { - AssertNormalizeNoTrim(@"1. a + [Test] + public void ListOrdered() + { + AssertNormalizeNoTrim(@"1. a 2. b 3. c"); - } + } - [Test] - public void ListOrderedAndIntended() - { - AssertNormalizeNoTrim(@"1. a + [Test] + public void ListOrderedAndIntended() + { + AssertNormalizeNoTrim(@"1. a 2. b - foo - bar @@ -218,169 +215,169 @@ 10. c - Bar 11. c 12. c"); - } + } - [Test] - public void HeaderAndParagraph() - { - AssertNormalizeNoTrim(@"# heading + [Test] + public void HeaderAndParagraph() + { + AssertNormalizeNoTrim(@"# heading paragraph paragraph2 without newlines"); - } + } - [Test] - public void QuoteBlock() - { - AssertNormalizeNoTrim(@"> test1 + [Test] + public void QuoteBlock() + { + AssertNormalizeNoTrim(@"> test1 > > test2"); - AssertNormalizeNoTrim(@"> test1 + AssertNormalizeNoTrim(@"> test1 This is a continuation > test2", - @"> test1 + @"> test1 > This is a continuation > test2" ); - AssertNormalizeNoTrim(@"> test1 + AssertNormalizeNoTrim(@"> test1 > -foobar asdf > test2 > -foobar sen."); - } + } - [Test] - public void ThematicBreak() - { - AssertNormalizeNoTrim("***\n"); + [Test] + public void ThematicBreak() + { + AssertNormalizeNoTrim("***\n"); - AssertNormalizeNoTrim("* * *\n", "***\n"); - } + AssertNormalizeNoTrim("* * *\n", "***\n"); + } - [Test] - public void AutolinkInline() - { - AssertNormalizeNoTrim("This has a "); - } + [Test] + public void AutolinkInline() + { + AssertNormalizeNoTrim("This has a "); + } - [Test] - public void CodeInline() - { - AssertNormalizeNoTrim("This has a ` ` in it"); - AssertNormalizeNoTrim("This has a `HelloWorld()` in it"); - AssertNormalizeNoTrim(@"This has a ``Hello`World()`` in it"); - AssertNormalizeNoTrim(@"This has a ``` Hello`World() ``` in it", @"This has a ``Hello`World()`` in it"); - AssertNormalizeNoTrim(@"This has a ``Hello`World()` `` in it"); - AssertNormalizeNoTrim(@"This has a ```` ``Hello```World()` ```` in it"); - AssertNormalizeNoTrim(@"This has a `` `Hello`World()`` in it"); - AssertNormalizeNoTrim(@"This has a ``` ``Hello`World()` ``` in it"); - } + [Test] + public void CodeInline() + { + AssertNormalizeNoTrim("This has a ` ` in it"); + AssertNormalizeNoTrim("This has a `HelloWorld()` in it"); + AssertNormalizeNoTrim(@"This has a ``Hello`World()`` in it"); + AssertNormalizeNoTrim(@"This has a ``` Hello`World() ``` in it", @"This has a ``Hello`World()`` in it"); + AssertNormalizeNoTrim(@"This has a ``Hello`World()` `` in it"); + AssertNormalizeNoTrim(@"This has a ```` ``Hello```World()` ```` in it"); + AssertNormalizeNoTrim(@"This has a `` `Hello`World()`` in it"); + AssertNormalizeNoTrim(@"This has a ``` ``Hello`World()` ``` in it"); + } - [Test] - public void EmphasisInline() - { - AssertNormalizeNoTrim("This is a plain **paragraph**"); - AssertNormalizeNoTrim("This is a plain *paragraph*"); - AssertNormalizeNoTrim("This is a plain _paragraph_"); - AssertNormalizeNoTrim("This is a plain __paragraph__"); - AssertNormalizeNoTrim("This is a pl*ai*n **paragraph**"); - } + [Test] + public void EmphasisInline() + { + AssertNormalizeNoTrim("This is a plain **paragraph**"); + AssertNormalizeNoTrim("This is a plain *paragraph*"); + AssertNormalizeNoTrim("This is a plain _paragraph_"); + AssertNormalizeNoTrim("This is a plain __paragraph__"); + AssertNormalizeNoTrim("This is a pl*ai*n **paragraph**"); + } - [Test] - public void LineBreakInline() - { - AssertNormalizeNoTrim("normal\nline break"); - AssertNormalizeNoTrim("hard \nline break"); - AssertNormalizeNoTrim("This is a hardline \nAnd this is another hardline\\\nThis is standard newline"); - AssertNormalizeNoTrim("This is a line\nWith another line\nAnd a last line"); - } + [Test] + public void LineBreakInline() + { + AssertNormalizeNoTrim("normal\nline break"); + AssertNormalizeNoTrim("hard \nline break"); + AssertNormalizeNoTrim("This is a hardline \nAnd this is another hardline\\\nThis is standard newline"); + AssertNormalizeNoTrim("This is a line\nWith another line\nAnd a last line"); + } - [Test] - public void LinkInline() - { - AssertNormalizeNoTrim("This is a [link](http://company.com)"); - AssertNormalizeNoTrim("This is an ![image](http://company.com)"); + [Test] + public void LinkInline() + { + AssertNormalizeNoTrim("This is a [link](http://company.com)"); + AssertNormalizeNoTrim("This is an ![image](http://company.com)"); - AssertNormalizeNoTrim(@"This is a [link](http://company.com ""Crazy Company"")"); - AssertNormalizeNoTrim(@"This is a [link](http://company.com ""Crazy \"" Company"")"); - } + AssertNormalizeNoTrim(@"This is a [link](http://company.com ""Crazy Company"")"); + AssertNormalizeNoTrim(@"This is a [link](http://company.com ""Crazy \"" Company"")"); + } - [Test] - public void LinkReferenceDefinition() - { - // Full link - AssertNormalizeNoTrim("This is a [link][MyLink]\n\n[MyLink]: http://company.com"); + [Test] + public void LinkReferenceDefinition() + { + // Full link + AssertNormalizeNoTrim("This is a [link][MyLink]\n\n[MyLink]: http://company.com"); - AssertNormalizeNoTrim("[MyLink]: http://company.com\nThis is a [link][MyLink]", - "This is a [link][MyLink]\n\n[MyLink]: http://company.com"); + AssertNormalizeNoTrim("[MyLink]: http://company.com\nThis is a [link][MyLink]", + "This is a [link][MyLink]\n\n[MyLink]: http://company.com"); - AssertNormalizeNoTrim("This is a [link][MyLink] a normal link [link](http://google.com) and another def link [link2][MyLink2]\n\n[MyLink]: http://company.com\n[MyLink2]: http://company2.com"); + AssertNormalizeNoTrim("This is a [link][MyLink] a normal link [link](http://google.com) and another def link [link2][MyLink2]\n\n[MyLink]: http://company.com\n[MyLink2]: http://company2.com"); - // Collapsed link - AssertNormalizeNoTrim("This is a [link][]\n\n[link]: http://company.com"); + // Collapsed link + AssertNormalizeNoTrim("This is a [link][]\n\n[link]: http://company.com"); - // Shortcut link - AssertNormalizeNoTrim("This is a [link]\n\n[link]: http://company.com"); - } + // Shortcut link + AssertNormalizeNoTrim("This is a [link]\n\n[link]: http://company.com"); + } - [Test] - public void EscapeInline() - { - AssertNormalizeNoTrim("This is an escape \\* with another \\["); - } + [Test] + public void EscapeInline() + { + AssertNormalizeNoTrim("This is an escape \\* with another \\["); + } - [Test] - public void HtmlEntityInline() - { - AssertNormalizeNoTrim("This is a ä blank"); - } + [Test] + public void HtmlEntityInline() + { + AssertNormalizeNoTrim("This is a ä blank"); + } - [Test] - public void HtmlInline() - { - AssertNormalizeNoTrim("foo
    bar"); - AssertNormalizeNoTrim(@"foo
    bar"); - } + [Test] + public void HtmlInline() + { + AssertNormalizeNoTrim("foo
    bar"); + AssertNormalizeNoTrim(@"foo
    bar"); + } - [Test] - public void SpaceBetweenNodes() - { - AssertNormalizeNoTrim("# Hello World\nFoobar is a better bar.", - "# Hello World\n\nFoobar is a better bar."); - } + [Test] + public void SpaceBetweenNodes() + { + AssertNormalizeNoTrim("# Hello World\nFoobar is a better bar.", + "# Hello World\n\nFoobar is a better bar."); + } - [Test] - public void SpaceBetweenNodesEvenForHeadlines() - { - AssertNormalizeNoTrim("# Hello World\n## Chapter 1\nFoobar is a better bar.", - "# Hello World\n\n## Chapter 1\n\nFoobar is a better bar."); - } + [Test] + public void SpaceBetweenNodesEvenForHeadlines() + { + AssertNormalizeNoTrim("# Hello World\n## Chapter 1\nFoobar is a better bar.", + "# Hello World\n\n## Chapter 1\n\nFoobar is a better bar."); + } - [Test] - public void SpaceRemoveAtStartAndEnd() - { - AssertNormalizeNoTrim("\n\n# Hello World\n## Chapter 1\nFoobar is a better bar.\n\n", - "# Hello World\n\n## Chapter 1\n\nFoobar is a better bar."); - } + [Test] + public void SpaceRemoveAtStartAndEnd() + { + AssertNormalizeNoTrim("\n\n# Hello World\n## Chapter 1\nFoobar is a better bar.\n\n", + "# Hello World\n\n## Chapter 1\n\nFoobar is a better bar."); + } - [Test] - public void SpaceShortenBetweenNodes() - { - AssertNormalizeNoTrim("# Hello World\n\n\n\nFoobar is a better bar.", - "# Hello World\n\nFoobar is a better bar."); - } + [Test] + public void SpaceShortenBetweenNodes() + { + AssertNormalizeNoTrim("# Hello World\n\n\n\nFoobar is a better bar.", + "# Hello World\n\nFoobar is a better bar."); + } - [Test] - public void BiggerSample() - { - var input = @"# Heading 1 + [Test] + public void BiggerSample() + { + var input = @"# Heading 1 This is a paragraph @@ -400,100 +397,99 @@ This is an indented code block line 2 of indented This is a last line"; - AssertNormalizeNoTrim(input); - } + AssertNormalizeNoTrim(input); + } - [Test] - public void TaskLists() - { - AssertNormalizeNoTrim("- [X] This is done"); - AssertNormalizeNoTrim("- [x] This is done", - "- [X] This is done"); - AssertNormalizeNoTrim("- [ ] This is not done"); - - // ignore - AssertNormalizeNoTrim("[x] This is not a task list"); - AssertNormalizeNoTrim("[ ] This is not a task list"); - } + [Test] + public void TaskLists() + { + AssertNormalizeNoTrim("- [X] This is done"); + AssertNormalizeNoTrim("- [x] This is done", + "- [X] This is done"); + AssertNormalizeNoTrim("- [ ] This is not done"); + + // ignore + AssertNormalizeNoTrim("[x] This is not a task list"); + AssertNormalizeNoTrim("[ ] This is not a task list"); + } - [Test] - public void JiraLinks() - { - AssertNormalizeNoTrim("FOO-1234"); - AssertNormalizeNoTrim("AB-1"); + [Test] + public void JiraLinks() + { + AssertNormalizeNoTrim("FOO-1234"); + AssertNormalizeNoTrim("AB-1"); - AssertNormalizeNoTrim("**Hello World AB-1**"); - } + AssertNormalizeNoTrim("**Hello World AB-1**"); + } + + [Test] + public void AutoLinks() + { + AssertNormalizeNoTrim("Hello from http://example.com/foo", "Hello from [http://example.com/foo](http://example.com/foo)", new NormalizeOptions() { ExpandAutoLinks = true, }); + AssertNormalizeNoTrim("Hello from www.example.com/foo", "Hello from [www.example.com/foo](http://www.example.com/foo)", new NormalizeOptions() { ExpandAutoLinks = true, }); + AssertNormalizeNoTrim("Hello from ftp://example.com", "Hello from [ftp://example.com](ftp://example.com)", new NormalizeOptions() { ExpandAutoLinks = true, }); + AssertNormalizeNoTrim("Hello from mailto:hello@example.com", "Hello from [hello@example.com](mailto:hello@example.com)", new NormalizeOptions() { ExpandAutoLinks = true, }); + + AssertNormalizeNoTrim("Hello from http://example.com/foo", "Hello from http://example.com/foo", new NormalizeOptions() { ExpandAutoLinks = false, }); + AssertNormalizeNoTrim("Hello from www.example.com/foo", "Hello from http://www.example.com/foo", new NormalizeOptions() { ExpandAutoLinks = false, }); + AssertNormalizeNoTrim("Hello from mailto:hello@example.com", "Hello from mailto:hello@example.com", new NormalizeOptions() { ExpandAutoLinks = false, }); + } - [Test] - public void AutoLinks() + private static void AssertSyntax(string expected, MarkdownObject syntax) + { + var writer = new StringWriter(); + var normalizer = new NormalizeRenderer(writer); + var document = new MarkdownDocument(); + if (syntax is Block) { - AssertNormalizeNoTrim("Hello from http://example.com/foo", "Hello from [http://example.com/foo](http://example.com/foo)", new NormalizeOptions() { ExpandAutoLinks = true, }); - AssertNormalizeNoTrim("Hello from www.example.com/foo", "Hello from [www.example.com/foo](http://www.example.com/foo)", new NormalizeOptions() { ExpandAutoLinks = true, }); - AssertNormalizeNoTrim("Hello from ftp://example.com", "Hello from [ftp://example.com](ftp://example.com)", new NormalizeOptions() { ExpandAutoLinks = true, }); - AssertNormalizeNoTrim("Hello from mailto:hello@example.com", "Hello from [hello@example.com](mailto:hello@example.com)", new NormalizeOptions() { ExpandAutoLinks = true, }); - - AssertNormalizeNoTrim("Hello from http://example.com/foo", "Hello from http://example.com/foo", new NormalizeOptions() { ExpandAutoLinks = false, }); - AssertNormalizeNoTrim("Hello from www.example.com/foo", "Hello from http://www.example.com/foo", new NormalizeOptions() { ExpandAutoLinks = false, }); - AssertNormalizeNoTrim("Hello from mailto:hello@example.com", "Hello from mailto:hello@example.com", new NormalizeOptions() { ExpandAutoLinks = false, }); + document.Add(syntax as Block); } - - private static void AssertSyntax(string expected, MarkdownObject syntax) + else { - var writer = new StringWriter(); - var normalizer = new NormalizeRenderer(writer); - var document = new MarkdownDocument(); - if (syntax is Block) - { - document.Add(syntax as Block); - } - else - { - throw new InvalidOperationException(); - } - normalizer.Render(document); + throw new InvalidOperationException(); + } + normalizer.Render(document); - var actual = writer.ToString(); + var actual = writer.ToString(); - Assert.AreEqual(expected, actual); - } + Assert.AreEqual(expected, actual); + } - public static void TestSpec(string inputText, string expectedOutputText, string extensions = null, string context = null) + public static void TestSpec(string inputText, string expectedOutputText, string extensions = null, string context = null) + { + foreach (var pipeline in TestParser.GetPipeline(extensions)) { - foreach (var pipeline in TestParser.GetPipeline(extensions)) - { - AssertNormalize(inputText, expectedOutputText, trim: false, pipeline: pipeline.Value, context: context); - } + AssertNormalize(inputText, expectedOutputText, trim: false, pipeline: pipeline.Value, context: context); } + } - public static void AssertNormalizeNoTrim(string input, string expected = null, NormalizeOptions options = null) - => AssertNormalize(input, expected, false, options); + public static void AssertNormalizeNoTrim(string input, string expected = null, NormalizeOptions options = null) + => AssertNormalize(input, expected, false, options); - public static void AssertNormalize(string input, string expected = null, bool trim = true, NormalizeOptions options = null, MarkdownPipeline pipeline = null, string context = null) - { - expected = expected ?? input; - input = NormText(input, trim); - expected = NormText(expected, trim); + public static void AssertNormalize(string input, string expected = null, bool trim = true, NormalizeOptions options = null, MarkdownPipeline pipeline = null, string context = null) + { + expected = expected ?? input; + input = NormText(input, trim); + expected = NormText(expected, trim); - pipeline = pipeline ?? new MarkdownPipelineBuilder() - .UseAutoLinks() - .UseJiraLinks(new Extensions.JiraLinks.JiraLinkOptions("https://jira.example.com")) - .UseTaskLists() - .Build(); + pipeline = pipeline ?? new MarkdownPipelineBuilder() + .UseAutoLinks() + .UseJiraLinks(new Extensions.JiraLinks.JiraLinkOptions("https://jira.example.com")) + .UseTaskLists() + .Build(); - var result = Markdown.Normalize(input, options, pipeline: pipeline); - result = NormText(result, trim); + var result = Markdown.Normalize(input, options, pipeline: pipeline); + result = NormText(result, trim); - TestParser.PrintAssertExpected(input, result, expected, context); - } + TestParser.PrintAssertExpected(input, result, expected, context); + } - private static string NormText(string text, bool trim) + private static string NormText(string text, bool trim) + { + if (trim) { - if (trim) - { - text = text.Trim(); - } - return text.Replace("\r\n", "\n").Replace('\r', '\n'); + text = text.Trim(); } + return text.Replace("\r\n", "\n").Replace('\r', '\n'); } } \ No newline at end of file diff --git a/src/Markdig.Tests/TestOrderedList.cs b/src/Markdig.Tests/TestOrderedList.cs index 125cf31fd..512f94aa2 100644 --- a/src/Markdig.Tests/TestOrderedList.cs +++ b/src/Markdig.Tests/TestOrderedList.cs @@ -1,43 +1,41 @@ -// Copyright (c) Alexandre Mutel. All rights reserved. +// 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.Helpers; -using NUnit.Framework; -namespace Markdig.Tests +namespace Markdig.Tests; + +[TestFixture] +public class TestOrderedList { - [TestFixture] - public class TestOrderedList + [Test] + public void TestReplace() { - [Test] - public void TestReplace() + var list = new OrderedList { - var list = new OrderedList - { - new A(), - new B(), - new C(), - }; - - // Replacing B with D. Order should now be A, D, B. - var result = list.Replace(new D()); - Assert.That(result, Is.True); - Assert.That(list.Count, Is.EqualTo(3)); - Assert.That(list[0], Is.InstanceOf()); - Assert.That(list[1], Is.InstanceOf()); - Assert.That(list[2], Is.InstanceOf()); + new A(), + new B(), + new C(), + }; - // Replacing B again should fail, as it's no longer in the list. - Assert.That(list.Replace(new D()), Is.False); - } + // Replacing B with D. Order should now be A, D, B. + var result = list.Replace(new D()); + Assert.That(result, Is.True); + Assert.That(list.Count, Is.EqualTo(3)); + Assert.That(list[0], Is.InstanceOf()); + Assert.That(list[1], Is.InstanceOf()); + Assert.That(list[2], Is.InstanceOf()); - #region Test fixtures - private interface ITest { } - private class A : ITest { } - private class B : ITest { } - private class C : ITest { } - private class D : ITest { } - #endregion + // Replacing B again should fail, as it's no longer in the list. + Assert.That(list.Replace(new D()), Is.False); } + + #region Test fixtures + private interface ITest { } + private class A : ITest { } + private class B : ITest { } + private class C : ITest { } + private class D : ITest { } + #endregion } \ No newline at end of file diff --git a/src/Markdig.Tests/TestParser.cs b/src/Markdig.Tests/TestParser.cs index 3d509c59a..9469e4432 100644 --- a/src/Markdig.Tests/TestParser.cs +++ b/src/Markdig.Tests/TestParser.cs @@ -1,201 +1,196 @@ // 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.Collections.Generic; -using System.IO; -using System.Linq; using System.Text; using System.Text.RegularExpressions; + using Markdig.Extensions.JiraLinks; using Markdig.Renderers.Roundtrip; using Markdig.Syntax; -using NUnit.Framework; -namespace Markdig.Tests +namespace Markdig.Tests; + +public class TestParser { - public class TestParser + [Test] + public void EnsureSpecsAreUpToDate() { - [Test] - public void EnsureSpecsAreUpToDate() - { - // In CI, SpecFileGen is guaranteed to run - if (IsContinuousIntegration) - return; + // In CI, SpecFileGen is guaranteed to run + if (IsContinuousIntegration) + return; - var specsFilePaths = Directory.GetDirectories(TestsDirectory) - .Where(dir => dir.EndsWith("Specs")) - .SelectMany(dir => Directory.GetFiles(dir) - .Where(file => file.EndsWith(".md", StringComparison.OrdinalIgnoreCase)) - .Where(file => file.IndexOf("readme", StringComparison.OrdinalIgnoreCase) == -1)) - .ToArray(); + var specsFilePaths = Directory.GetDirectories(TestsDirectory) + .Where(dir => dir.EndsWith("Specs")) + .SelectMany(dir => Directory.GetFiles(dir) + .Where(file => file.EndsWith(".md", StringComparison.OrdinalIgnoreCase)) + .Where(file => file.IndexOf("readme", StringComparison.OrdinalIgnoreCase) == -1)) + .ToArray(); - var specsMarkdown = new string[specsFilePaths.Length]; - var specsSyntaxTrees = new MarkdownDocument[specsFilePaths.Length]; + var specsMarkdown = new string[specsFilePaths.Length]; + var specsSyntaxTrees = new MarkdownDocument[specsFilePaths.Length]; - var pipeline = new MarkdownPipelineBuilder() - .UseAdvancedExtensions() - .Build(); - - for (int i = 0; i < specsFilePaths.Length; i++) - { - string markdown = specsMarkdown[i] = File.ReadAllText(specsFilePaths[i]); - specsSyntaxTrees[i] = Markdown.Parse(markdown, pipeline); - } + var pipeline = new MarkdownPipelineBuilder() + .UseAdvancedExtensions() + .Build(); - foreach (var specFilePath in specsFilePaths) - { - string testFilePath = Path.ChangeExtension(specFilePath, ".generated.cs"); + for (int i = 0; i < specsFilePaths.Length; i++) + { + string markdown = specsMarkdown[i] = File.ReadAllText(specsFilePaths[i]); + specsSyntaxTrees[i] = Markdown.Parse(markdown, pipeline); + } - Assert.True(File.Exists(testFilePath), - "A new specification file has been added. Add the spec to the list in SpecFileGen and regenerate the tests."); + foreach (var specFilePath in specsFilePaths) + { + string testFilePath = Path.ChangeExtension(specFilePath, ".generated.cs"); - DateTime specTime = File.GetLastWriteTimeUtc(specFilePath); - DateTime testTime = File.GetLastWriteTimeUtc(testFilePath); + Assert.True(File.Exists(testFilePath), + "A new specification file has been added. Add the spec to the list in SpecFileGen and regenerate the tests."); - // If file creation times aren't preserved by git, add some leeway - // If specs have come from git, assume that they were regenerated since CI would fail otherwise - testTime = testTime.AddMinutes(3); + DateTime specTime = File.GetLastWriteTimeUtc(specFilePath); + DateTime testTime = File.GetLastWriteTimeUtc(testFilePath); - // This might not catch a changed spec every time, but should at least sometimes. Otherwise CI will catch it + // If file creation times aren't preserved by git, add some leeway + // If specs have come from git, assume that they were regenerated since CI would fail otherwise + testTime = testTime.AddMinutes(3); - // This could also trigger, if a user has modified the spec file but reverted the change - can't think of a good workaround - Assert.Less(specTime, testTime, - $"{Path.GetFileName(specFilePath)} has been modified. Run SpecFileGen to regenerate the tests. " + - "If you have modified a specification file, but reverted all changes, ignore this error or revert the 'changed' timestamp metadata on the file."); - } + // This might not catch a changed spec every time, but should at least sometimes. Otherwise CI will catch it - TestDescendantsOrder.TestSchemas(specsSyntaxTrees); + // This could also trigger, if a user has modified the spec file but reverted the change - can't think of a good workaround + Assert.Less(specTime, testTime, + $"{Path.GetFileName(specFilePath)} has been modified. Run SpecFileGen to regenerate the tests. " + + "If you have modified a specification file, but reverted all changes, ignore this error or revert the 'changed' timestamp metadata on the file."); } - [Test] - public void ParseEmptyDocumentWithTrackTriviaEnabled() + TestDescendantsOrder.TestSchemas(specsSyntaxTrees); + } + + [Test] + public void ParseEmptyDocumentWithTrackTriviaEnabled() + { + var document = Markdown.Parse("", trackTrivia: true); + using var sw = new StringWriter(); + new RoundtripRenderer(sw).Render(document); + Assert.AreEqual("", sw.ToString()); + } + + public static void TestSpec(string inputText, string expectedOutputText, string extensions = null, bool plainText = false, string context = null) + { + context ??= string.Empty; + if (!string.IsNullOrEmpty(context)) { - var document = Markdown.Parse("", trackTrivia: true); - using var sw = new StringWriter(); - new RoundtripRenderer(sw).Render(document); - Assert.AreEqual("", sw.ToString()); + context += "\n"; } - - public static void TestSpec(string inputText, string expectedOutputText, string extensions = null, bool plainText = false, string context = null) + foreach (var pipeline in GetPipeline(extensions)) { - context ??= string.Empty; - if (!string.IsNullOrEmpty(context)) - { - context += "\n"; - } - foreach (var pipeline in GetPipeline(extensions)) - { - TestSpec(inputText, expectedOutputText, pipeline.Value, plainText, context: context + $"Pipeline configured with extensions: {pipeline.Key}"); - } + TestSpec(inputText, expectedOutputText, pipeline.Value, plainText, context: context + $"Pipeline configured with extensions: {pipeline.Key}"); } + } - public static void TestSpec(string inputText, string expectedOutputText, MarkdownPipeline pipeline, bool plainText = false, string context = null) - { - // Uncomment this line to get more debug information for process inlines. - //pipeline.DebugLog = Console.Out; - var result = plainText ? Markdown.ToPlainText(inputText, pipeline) : Markdown.ToHtml(inputText, pipeline); + public static void TestSpec(string inputText, string expectedOutputText, MarkdownPipeline pipeline, bool plainText = false, string context = null) + { + // Uncomment this line to get more debug information for process inlines. + //pipeline.DebugLog = Console.Out; + var result = plainText ? Markdown.ToPlainText(inputText, pipeline) : Markdown.ToHtml(inputText, pipeline); - result = Compact(result); - expectedOutputText = Compact(expectedOutputText); + result = Compact(result); + expectedOutputText = Compact(expectedOutputText); - PrintAssertExpected(inputText, result, expectedOutputText, context); - } + PrintAssertExpected(inputText, result, expectedOutputText, context); + } - public static void PrintAssertExpected(string source, string result, string expected, string context = null) + public static void PrintAssertExpected(string source, string result, string expected, string context = null) + { + if (expected != result) { - if (expected != result) + if (context != null) { - if (context != null) - { - Console.WriteLine(context); - } - Console.WriteLine("```````````````````Source"); - Console.WriteLine(DisplaySpaceAndTabs(source)); - Console.WriteLine("```````````````````Result"); - Console.WriteLine(DisplaySpaceAndTabs(result)); - Console.WriteLine("```````````````````Expected"); - Console.WriteLine(DisplaySpaceAndTabs(expected)); - Console.WriteLine("```````````````````"); - Console.WriteLine(); - TextAssert.AreEqual(expected, result); + Console.WriteLine(context); } + Console.WriteLine("```````````````````Source"); + Console.WriteLine(DisplaySpaceAndTabs(source)); + Console.WriteLine("```````````````````Result"); + Console.WriteLine(DisplaySpaceAndTabs(result)); + Console.WriteLine("```````````````````Expected"); + Console.WriteLine(DisplaySpaceAndTabs(expected)); + Console.WriteLine("```````````````````"); + Console.WriteLine(); + TextAssert.AreEqual(expected, result); + } + } + + public static IEnumerable> GetPipeline(string extensionsGroupText) + { + // For the standard case, we make sure that both the CommmonMark core and Extra/Advanced are CommonMark compliant! + if (string.IsNullOrEmpty(extensionsGroupText)) + { + yield return new KeyValuePair("default", new MarkdownPipelineBuilder().Build()); + + //yield return new KeyValuePair("default-trivia", new MarkdownPipelineBuilder().EnableTrackTrivia().Build()); + + yield return new KeyValuePair("advanced", new MarkdownPipelineBuilder() // Use similar to advanced extension without auto-identifier + .UseAbbreviations() + //.UseAutoIdentifiers() + .UseCitations() + .UseCustomContainers() + .UseDefinitionLists() + .UseEmphasisExtras() + .UseFigures() + .UseFooters() + .UseFootnotes() + .UseGridTables() + .UseMathematics() + .UseMediaLinks() + .UsePipeTables() + .UseListExtras() + .UseGenericAttributes().Build()); + + yield break; } - public static IEnumerable> GetPipeline(string extensionsGroupText) + var extensionGroups = extensionsGroupText.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); + foreach (var extensionsText in extensionGroups) { - // For the standard case, we make sure that both the CommmonMark core and Extra/Advanced are CommonMark compliant! - if (string.IsNullOrEmpty(extensionsGroupText)) + var builder = new MarkdownPipelineBuilder(); + builder.DebugLog = Console.Out; + if (extensionsText == "jiralinks") { - yield return new KeyValuePair("default", new MarkdownPipelineBuilder().Build()); - - //yield return new KeyValuePair("default-trivia", new MarkdownPipelineBuilder().EnableTrackTrivia().Build()); - - yield return new KeyValuePair("advanced", new MarkdownPipelineBuilder() // Use similar to advanced extension without auto-identifier - .UseAbbreviations() - //.UseAutoIdentifiers() - .UseCitations() - .UseCustomContainers() - .UseDefinitionLists() - .UseEmphasisExtras() - .UseFigures() - .UseFooters() - .UseFootnotes() - .UseGridTables() - .UseMathematics() - .UseMediaLinks() - .UsePipeTables() - .UseListExtras() - .UseGenericAttributes().Build()); - - yield break; + builder.UseJiraLinks(new JiraLinkOptions("http://your.company.abc")); } - - var extensionGroups = extensionsGroupText.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); - foreach (var extensionsText in extensionGroups) + else { - var builder = new MarkdownPipelineBuilder(); - builder.DebugLog = Console.Out; - if (extensionsText == "jiralinks") - { - builder.UseJiraLinks(new JiraLinkOptions("http://your.company.abc")); - } - else - { - builder = extensionsText == "self" ? builder.UseSelfPipeline() : builder.Configure(extensionsText); - } - yield return new KeyValuePair(extensionsText, builder.Build()); + builder = extensionsText == "self" ? builder.UseSelfPipeline() : builder.Configure(extensionsText); } + yield return new KeyValuePair(extensionsText, builder.Build()); } + } - public static string DisplaySpaceAndTabs(string text) - { - // Output special characters to check correctly the results - return text.Replace('\t', '→').Replace(' ', '·'); - } + public static string DisplaySpaceAndTabs(string text) + { + // Output special characters to check correctly the results + return text.Replace('\t', '→').Replace(' ', '·'); + } - private static string Compact(string html) - { - // Normalize the output to make it compatible with CommonMark specs - html = html.Replace("\r\n", "\n").Replace(@"\r", @"\n").Trim(); - html = Regex.Replace(html, @"\s+", ""); - html = Regex.Replace(html, @"
  1. \s+", "
  2. "); - html = html.Normalize(NormalizationForm.FormKD); - return html; - } + private static string Compact(string html) + { + // Normalize the output to make it compatible with CommonMark specs + html = html.Replace("\r\n", "\n").Replace(@"\r", @"\n").Trim(); + html = Regex.Replace(html, @"\s+
  3. ", ""); + html = Regex.Replace(html, @"
  4. \s+", "
  5. "); + html = html.Normalize(NormalizationForm.FormKD); + return html; + } - public static readonly bool IsContinuousIntegration = Environment.GetEnvironmentVariable("CI") != null; + public static readonly bool IsContinuousIntegration = Environment.GetEnvironmentVariable("CI") != null; - public static readonly string TestsDirectory = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(typeof(TestParser).Assembly.Location), "../../..")); + public static readonly string TestsDirectory = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(typeof(TestParser).Assembly.Location), "../../..")); - static TestParser() + static TestParser() + { + const string RunningInsideVisualStudioPath = "\\src\\.vs\\markdig\\"; + int index = TestsDirectory.IndexOf(RunningInsideVisualStudioPath); + if (index != -1) { - const string RunningInsideVisualStudioPath = "\\src\\.vs\\markdig\\"; - int index = TestsDirectory.IndexOf(RunningInsideVisualStudioPath); - if (index != -1) - { - TestsDirectory = TestsDirectory.Substring(0, index) + "\\src\\Markdig.Tests"; - } + TestsDirectory = TestsDirectory.Substring(0, index) + "\\src\\Markdig.Tests"; } } } \ No newline at end of file diff --git a/src/Markdig.Tests/TestPipeTable.cs b/src/Markdig.Tests/TestPipeTable.cs index 5b683927a..cffd479cf 100644 --- a/src/Markdig.Tests/TestPipeTable.cs +++ b/src/Markdig.Tests/TestPipeTable.cs @@ -1,24 +1,21 @@ using Markdig.Extensions.Tables; using Markdig.Syntax; -using NUnit.Framework; -using System.Linq; -namespace Markdig.Tests +namespace Markdig.Tests; + +[TestFixture] +public sealed class TestPipeTable { - [TestFixture] - public sealed class TestPipeTable + [TestCase("| S | T |\r\n|---|---| \r\n| G | H |")] + [TestCase("| S | T |\r\n|---|---|\t\r\n| G | H |")] + [TestCase("| S | T |\r\n|---|---|\f\r\n| G | H |")] + [TestCase("| S | \r\n|---|\r\n| G |\r\n\r\n| D | D |\r\n| ---| ---| \r\n| V | V |", 2)] + public void TestTableBug(string markdown, int tableCount = 1) { - [TestCase("| S | T |\r\n|---|---| \r\n| G | H |")] - [TestCase("| S | T |\r\n|---|---|\t\r\n| G | H |")] - [TestCase("| S | T |\r\n|---|---|\f\r\n| G | H |")] - [TestCase("| S | \r\n|---|\r\n| G |\r\n\r\n| D | D |\r\n| ---| ---| \r\n| V | V |", 2)] - public void TestTableBug(string markdown, int tableCount = 1) - { - MarkdownDocument document = Markdown.Parse(markdown, new MarkdownPipelineBuilder().UseAdvancedExtensions().Build()); + MarkdownDocument document = Markdown.Parse(markdown, new MarkdownPipelineBuilder().UseAdvancedExtensions().Build()); - Table[] tables = document.Descendants().OfType().ToArray(); + Table[] tables = document.Descendants().OfType
    ().ToArray(); - Assert.AreEqual(tableCount, tables.Length); - } + Assert.AreEqual(tableCount, tables.Length); } } diff --git a/src/Markdig.Tests/TestPlainText.cs b/src/Markdig.Tests/TestPlainText.cs index ddca08b93..3aa78299a 100644 --- a/src/Markdig.Tests/TestPlainText.cs +++ b/src/Markdig.Tests/TestPlainText.cs @@ -1,45 +1,42 @@ -using NUnit.Framework; +namespace Markdig.Tests; -namespace Markdig.Tests +[TestFixture] +public class TestPlainText { - [TestFixture] - public class TestPlainText + [Test] + [TestCase(/* markdownText: */ "foo bar", /* expected: */ "foo bar\n")] + [TestCase(/* markdownText: */ "foo\nbar", /* expected: */ "foo\nbar\n")] + [TestCase(/* markdownText: */ "*foo\nbar*", /* expected: */ "foo\nbar\n")] + [TestCase(/* markdownText: */ "[foo\nbar](http://example.com)", /* expected: */ "foo\nbar\n")] + [TestCase(/* markdownText: */ "", /* expected: */ "http://foo.bar.baz\n")] + [TestCase(/* markdownText: */ "# foo bar", /* expected: */ "foo bar\n")] + [TestCase(/* markdownText: */ "# foo\nbar", /* expected: */ "foo\nbar\n")] + [TestCase(/* markdownText: */ "> foo", /* expected: */ "foo\n")] + [TestCase(/* markdownText: */ "> foo\nbar\n> baz", /* expected: */ "foo\nbar\nbaz\n")] + [TestCase(/* markdownText: */ "`foo`", /* expected: */ "foo\n")] + [TestCase(/* markdownText: */ "`foo\nbar`", /* expected: */ "foo bar\n")] // new line within codespan is treated as whitespace (Example317) + [TestCase(/* markdownText: */ "```\nfoo bar\n```", /* expected: */ "foo bar\n")] + [TestCase(/* markdownText: */ "- foo\n- bar\n- baz", /* expected: */ "foo\nbar\nbaz\n")] + [TestCase(/* markdownText: */ "- foo`", /* expected: */ "foo bar::baz >\n")] + public void TestPlainEnsureNewLine(string markdownText, string expected) { - [Test] - [TestCase(/* markdownText: */ "foo bar", /* expected: */ "foo bar\n")] - [TestCase(/* markdownText: */ "foo\nbar", /* expected: */ "foo\nbar\n")] - [TestCase(/* markdownText: */ "*foo\nbar*", /* expected: */ "foo\nbar\n")] - [TestCase(/* markdownText: */ "[foo\nbar](http://example.com)", /* expected: */ "foo\nbar\n")] - [TestCase(/* markdownText: */ "", /* expected: */ "http://foo.bar.baz\n")] - [TestCase(/* markdownText: */ "# foo bar", /* expected: */ "foo bar\n")] - [TestCase(/* markdownText: */ "# foo\nbar", /* expected: */ "foo\nbar\n")] - [TestCase(/* markdownText: */ "> foo", /* expected: */ "foo\n")] - [TestCase(/* markdownText: */ "> foo\nbar\n> baz", /* expected: */ "foo\nbar\nbaz\n")] - [TestCase(/* markdownText: */ "`foo`", /* expected: */ "foo\n")] - [TestCase(/* markdownText: */ "`foo\nbar`", /* expected: */ "foo bar\n")] // new line within codespan is treated as whitespace (Example317) - [TestCase(/* markdownText: */ "```\nfoo bar\n```", /* expected: */ "foo bar\n")] - [TestCase(/* markdownText: */ "- foo\n- bar\n- baz", /* expected: */ "foo\nbar\nbaz\n")] - [TestCase(/* markdownText: */ "- foo`", /* expected: */ "foo bar::baz >\n")] - public void TestPlainEnsureNewLine(string markdownText, string expected) - { - var actual = Markdown.ToPlainText(markdownText); - Assert.AreEqual(expected, actual); - } + var actual = Markdown.ToPlainText(markdownText); + Assert.AreEqual(expected, actual); + } - [Test] - [TestCase(/* markdownText: */ ":::\nfoo\n:::", /* expected: */ "foo\n", /*extensions*/ "customcontainers|advanced")] - [TestCase(/* markdownText: */ ":::bar\nfoo\n:::", /* expected: */ "foo\n", /*extensions*/ "customcontainers+attributes|advanced")] - [TestCase(/* markdownText: */ "| Header1 | Header2 | Header3 |\n|--|--|--|\nt**es**t|value2|value3", /* expected: */ "Header1 Header2 Header3 test value2 value3","pipetables")] - public void TestPlainWithExtensions(string markdownText, string expected, string extensions) - { - TestParser.TestSpec(markdownText, expected, extensions, plainText: true); - } + [Test] + [TestCase(/* markdownText: */ ":::\nfoo\n:::", /* expected: */ "foo\n", /*extensions*/ "customcontainers|advanced")] + [TestCase(/* markdownText: */ ":::bar\nfoo\n:::", /* expected: */ "foo\n", /*extensions*/ "customcontainers+attributes|advanced")] + [TestCase(/* markdownText: */ "| Header1 | Header2 | Header3 |\n|--|--|--|\nt**es**t|value2|value3", /* expected: */ "Header1 Header2 Header3 test value2 value3","pipetables")] + public void TestPlainWithExtensions(string markdownText, string expected, string extensions) + { + TestParser.TestSpec(markdownText, expected, extensions, plainText: true); + } - public static void TestSpec(string markdownText, string expected, string extensions, string context = null) - { - TestParser.TestSpec(markdownText, expected, extensions, plainText: true, context: context); - } + public static void TestSpec(string markdownText, string expected, string extensions, string context = null) + { + TestParser.TestSpec(markdownText, expected, extensions, plainText: true, context: context); } } diff --git a/src/Markdig.Tests/TestPlayParser.cs b/src/Markdig.Tests/TestPlayParser.cs index cd45e64ab..3d90a3004 100644 --- a/src/Markdig.Tests/TestPlayParser.cs +++ b/src/Markdig.Tests/TestPlayParser.cs @@ -1,97 +1,94 @@ // 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 Markdig.Syntax; using Markdig.Syntax.Inlines; -using NUnit.Framework; -namespace Markdig.Tests +namespace Markdig.Tests; + +[TestFixture] +public class TestPlayParser { - [TestFixture] - public class TestPlayParser + [Test] + public void TestBugWithEmphasisAndTable() + { + TestParser.TestSpec("**basics | 8:00**", "

    basics | 8:00

    ", "advanced"); + } + + [Test] + public void TestLinksWithCarriageReturn() + { + var text = "[Link 1][link-1], [link 2][link-2].\r\n\r\n[link-1]: https://example.com\r\n[link-2]: https://example.com"; + var result = Markdown.ToHtml(text).TrimEnd(); + Assert.AreEqual("

    Link 1, link 2.

    ", result); + } + + [Test] + public void TestLinksWithTitleAndCarriageReturn() + { + var text = "[Link 1][link-1], [link 2][link-2].\r\n\r\n[link-1]: https://example.com \"title 1\" \r\n[link-2]: https://example.com \"title 2\""; + var result = Markdown.ToHtml(text).TrimEnd(); + Assert.AreEqual("

    Link 1, link 2.

    ", result); + } + + [Test] + public void TestLink() { - [Test] - public void TestBugWithEmphasisAndTable() - { - TestParser.TestSpec("**basics | 8:00**", "

    basics | 8:00

    ", "advanced"); - } - - [Test] - public void TestLinksWithCarriageReturn() - { - var text = "[Link 1][link-1], [link 2][link-2].\r\n\r\n[link-1]: https://example.com\r\n[link-2]: https://example.com"; - var result = Markdown.ToHtml(text).TrimEnd(); - Assert.AreEqual("

    Link 1, link 2.

    ", result); - } - - [Test] - public void TestLinksWithTitleAndCarriageReturn() - { - var text = "[Link 1][link-1], [link 2][link-2].\r\n\r\n[link-1]: https://example.com \"title 1\" \r\n[link-2]: https://example.com \"title 2\""; - var result = Markdown.ToHtml(text).TrimEnd(); - Assert.AreEqual("

    Link 1, link 2.

    ", result); - } - - [Test] - public void TestLink() - { - var doc = Markdown.Parse("There is a ![link](/yoyo)"); - var link = doc.Descendants().SelectMany(x => x.Inline.Descendants()).FirstOrDefault(l => l.IsImage); - Assert.AreEqual("/yoyo", link?.Url); - } - - [Test] - public void TestListBug2() - { - TestParser.TestSpec("10.\t*test* – test\n\n11.\t__test__ test\n\n", @"
      + var doc = Markdown.Parse("There is a ![link](/yoyo)"); + var link = doc.Descendants().SelectMany(x => x.Inline.Descendants()).FirstOrDefault(l => l.IsImage); + Assert.AreEqual("/yoyo", link?.Url); + } + + [Test] + public void TestListBug2() + { + TestParser.TestSpec("10.\t*test* – test\n\n11.\t__test__ test\n\n", @"
      1. test – test

      2. test test

      "); - } + } - [Test] - public void TestSimple() - { - var text = @" *[HTML]: Hypertext Markup Language + [Test] + public void TestSimple() + { + var text = @" *[HTML]: Hypertext Markup Language Later in a text we are using HTML and it becomes an abbr tag HTML "; - // var reader = new StringReader(@"> > toto tata - //> titi toto - //"); - - //var result = Markdown.ToHtml(text, new MarkdownPipeline().UseFootnotes().UseEmphasisExtras()); - var result = Markdown.ToHtml(text, new MarkdownPipelineBuilder().UseAbbreviations().Build()); - //File.WriteAllText("test.html", result, Encoding.UTF8); - //Console.WriteLine(result); - } - - [Test] - public void TestEmptyLiteral() - { - var text = @"> *some text* + // var reader = new StringReader(@"> > toto tata + //> titi toto + //"); + + //var result = Markdown.ToHtml(text, new MarkdownPipeline().UseFootnotes().UseEmphasisExtras()); + var result = Markdown.ToHtml(text, new MarkdownPipelineBuilder().UseAbbreviations().Build()); + //File.WriteAllText("test.html", result, Encoding.UTF8); + //Console.WriteLine(result); + } + + [Test] + public void TestEmptyLiteral() + { + var text = @"> *some text* > some other text"; - var doc = Markdown.Parse(text); + var doc = Markdown.Parse(text); - Assert.True(doc.Descendants().All(x => !x.Content.IsEmpty), - "There should not have any empty literals"); - } + Assert.True(doc.Descendants().All(x => !x.Content.IsEmpty), + "There should not have any empty literals"); + } - [Test] - public void TestSelfPipeline1() - { - var text = @" + [Test] + public void TestSelfPipeline1() + { + var text = @" a | b - | - 0 | 1 "; - TestParser.TestSpec(text, @" + TestParser.TestSpec(text, @"
    @@ -107,17 +104,17 @@ public void TestSelfPipeline1()
    ", "self"); - } + } - [Test] - public void TestListBug() - { - // TODO: Add this test back to the CommonMark specs - var text = @"- item1 + [Test] + public void TestListBug() + { + // TODO: Add this test back to the CommonMark specs + var text = @"- item1 - item2 - item3 - item4"; - TestParser.TestSpec(text, @"
      + TestParser.TestSpec(text, @"
      • item1
        • item2 @@ -130,13 +127,13 @@ public void TestListBug()
      "); - } + } - [Test] - public void TestHtmlBug() - { - TestParser.TestSpec(@" # header1 + [Test] + public void TestHtmlBug() + { + TestParser.TestSpec(@" # header1
       blabla
      @@ -148,43 +145,43 @@ public void TestHtmlBug()
       blabla
       

      header2

      "); - } + } - [Test] - public void TestHtmlh4Bug() - { - TestParser.TestSpec(@"

      foobar

      ", @"

      foobar

      "); - } + [Test] + public void TestHtmlh4Bug() + { + TestParser.TestSpec(@"

      foobar

      ", @"

      foobar

      "); + } - [Test] - public void TestStandardUriEscape() - { - TestParser.TestSpec(@"![你好](你好.png)", "

      \"你好\"

      ", "nonascii-noescape"); - } + [Test] + public void TestStandardUriEscape() + { + TestParser.TestSpec(@"![你好](你好.png)", "

      \"你好\"

      ", "nonascii-noescape"); + } - [Test] - public void TestBugAdvanced() - { - TestParser.TestSpec(@"`https://{domain}/callbacks` + [Test] + public void TestBugAdvanced() + { + TestParser.TestSpec(@"`https://{domain}/callbacks` #### HEADING Paragraph ", "

      https://{domain}/callbacks

      \n

      HEADING

      \n

      Paragraph

      ", "advanced"); - } + } - [Test] - public void TestBugEmphAttribute() - { - // https://github.com/lunet-io/markdig/issues/108 - TestParser.TestSpec(@"*test*{name=value}", "

      test

      ", "advanced"); - } + [Test] + public void TestBugEmphAttribute() + { + // https://github.com/lunet-io/markdig/issues/108 + TestParser.TestSpec(@"*test*{name=value}", "

      test

      ", "advanced"); + } - [Test] - public void TestBugPipeTables() - { - // https://github.com/lunet-io/markdig/issues/73 - TestParser.TestSpec(@"| abc | def | + [Test] + public void TestBugPipeTables() + { + // https://github.com/lunet-io/markdig/issues/73 + TestParser.TestSpec(@"| abc | def | | --- | --- | | 1 | ~3 | ", @" @@ -201,12 +198,12 @@ public void TestBugPipeTables()
      ", "advanced"); - } + } - [Test] - public void TestGridTableWithCustomAttributes() { + [Test] + public void TestGridTableWithCustomAttributes() { - var input = @" + var input = @" {.table} +---+---+ | a | b | @@ -215,7 +212,7 @@ public void TestGridTableWithCustomAttributes() { +---+---+ "; - var expected = @" + var expected = @"
      @@ -232,76 +229,75 @@ public void TestGridTableWithCustomAttributes() {
      "; - TestParser.TestSpec(input, expected, "advanced"); - } + TestParser.TestSpec(input, expected, "advanced"); + } - [Test] - public void TestSamePipelineAllExtensions() - { - var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build(); + [Test] + public void TestSamePipelineAllExtensions() + { + var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build(); - // Reuse the same pipeline - var result1 = Markdown.ToHtml("This is a \"\"citation\"\"", pipeline); - var result2 = Markdown.ToHtml("This is a \"\"citation\"\"", pipeline); + // Reuse the same pipeline + var result1 = Markdown.ToHtml("This is a \"\"citation\"\"", pipeline); + var result2 = Markdown.ToHtml("This is a \"\"citation\"\"", pipeline); - Assert.AreEqual("

      This is a citation

      ", result1.Trim()); - Assert.AreEqual(result1, result2); - } + Assert.AreEqual("

      This is a citation

      ", result1.Trim()); + Assert.AreEqual(result1, result2); + } - // Test for emoji and smileys - // var text = @" This is a test with a :) and a :angry: smiley"; + // Test for emoji and smileys + // var text = @" This is a test with a :) and a :angry: smiley"; - // Test for definition lists: - // - // var text = @" - //Term 1 - //: This is a definition item - // With a paragraph - // > This is a block quote + // Test for definition lists: + // + // var text = @" + //Term 1 + //: This is a definition item + // With a paragraph + // > This is a block quote - // - This is a list - // - item2 + // - This is a list + // - item2 - // ```java - // Test + // ```java + // Test - // ``` + // ``` - // And a lazy line - //: This ia another definition item + // And a lazy line + //: This ia another definition item - //Term2 - //Term3 *with some inline* - //: This is another definition for term2 - //"; + //Term2 + //Term3 *with some inline* + //: This is another definition for term2 + //"; - // Test for grid table + // Test for grid table - // var text = @" - //+-----------------------------------+--------------------------------------+ - //| - this is a list | > We have a blockquote - //| - this is a second item | - //| | - //| ``` | - //| Yes | - //| ``` | - //+===================================+======================================+ - //| This is a second line | - //+-----------------------------------+--------------------------------------+ + // var text = @" + //+-----------------------------------+--------------------------------------+ + //| - this is a list | > We have a blockquote + //| - this is a second item | + //| | + //| ``` | + //| Yes | + //| ``` | + //+===================================+======================================+ + //| This is a second line | + //+-----------------------------------+--------------------------------------+ - //:::spoiler {#yessss} - //This is a spoiler - //::: + //:::spoiler {#yessss} + //This is a spoiler + //::: - ///| we have mult | paragraph | - ///| we have a new colspan with a long line - ///| and lots of text - //"; + ///| we have mult | paragraph | + ///| we have a new colspan with a long line + ///| and lots of text + //"; - } } \ No newline at end of file diff --git a/src/Markdig.Tests/TestPragmaLines.cs b/src/Markdig.Tests/TestPragmaLines.cs index ae079acd5..f8cc358bc 100644 --- a/src/Markdig.Tests/TestPragmaLines.cs +++ b/src/Markdig.Tests/TestPragmaLines.cs @@ -1,19 +1,18 @@ -// Copyright (c) Alexandre Mutel. All rights reserved. +// 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.Syntax; -using NUnit.Framework; -namespace Markdig.Tests +namespace Markdig.Tests; + +[TestFixture] +public class TestPragmaLines { - [TestFixture] - public class TestPragmaLines + [Test] + public void TestFindClosest() { - [Test] - public void TestFindClosest() - { - var doc = Markdown.Parse( + var doc = Markdown.Parse( "test1\n" + // 0 "\n" + // 1 "test2\n" + // 2 @@ -37,27 +36,27 @@ public void TestFindClosest() "- item3\n" + // 20 "\n" + // 21 "This is a last paragraph\n" // 22 - , new MarkdownPipelineBuilder().UsePragmaLines().Build()); + , new MarkdownPipelineBuilder().UsePragmaLines().Build()); - foreach (var exact in new int[] {0, 2, 4, 6, 8, 10, 18, 19, 20, 22}) - { - Assert.AreEqual(exact, doc.FindClosestLine(exact)); - } + foreach (var exact in new int[] {0, 2, 4, 6, 8, 10, 18, 19, 20, 22}) + { + Assert.AreEqual(exact, doc.FindClosestLine(exact)); + } - Assert.AreEqual(22, doc.FindClosestLine(23)); + Assert.AreEqual(22, doc.FindClosestLine(23)); - Assert.AreEqual(10, doc.FindClosestLine(11)); - Assert.AreEqual(10, doc.FindClosestLine(12)); - Assert.AreEqual(10, doc.FindClosestLine(13)); - Assert.AreEqual(18, doc.FindClosestLine(14)); // > 50% of the paragraph, we switch to next - Assert.AreEqual(18, doc.FindClosestLine(15)); - Assert.AreEqual(18, doc.FindClosestLine(16)); - } + Assert.AreEqual(10, doc.FindClosestLine(11)); + Assert.AreEqual(10, doc.FindClosestLine(12)); + Assert.AreEqual(10, doc.FindClosestLine(13)); + Assert.AreEqual(18, doc.FindClosestLine(14)); // > 50% of the paragraph, we switch to next + Assert.AreEqual(18, doc.FindClosestLine(15)); + Assert.AreEqual(18, doc.FindClosestLine(16)); + } - [Test] - public void TestFindClosest1() - { - var text = + [Test] + public void TestFindClosest1() + { + var text = "- item1\n" + // 0 " - item11\n" + // 1 " - item12\n" + // 2 @@ -66,15 +65,14 @@ public void TestFindClosest1() " - item131\n" + // 5 " - item1311\n"; // 6 - var pipeline = new MarkdownPipelineBuilder().UsePragmaLines().Build(); - var doc = Markdown.Parse(text, pipeline); - - for (int exact = 0; exact < 7; exact++) - { - Assert.AreEqual(exact, doc.FindClosestLine(exact)); - } + var pipeline = new MarkdownPipelineBuilder().UsePragmaLines().Build(); + var doc = Markdown.Parse(text, pipeline); - Assert.AreEqual(6, doc.FindClosestLine(50)); + for (int exact = 0; exact < 7; exact++) + { + Assert.AreEqual(exact, doc.FindClosestLine(exact)); } + + Assert.AreEqual(6, doc.FindClosestLine(50)); } } \ No newline at end of file diff --git a/src/Markdig.Tests/TestReferralLinks.cs b/src/Markdig.Tests/TestReferralLinks.cs index 5e06ea13b..855cf1fdb 100644 --- a/src/Markdig.Tests/TestReferralLinks.cs +++ b/src/Markdig.Tests/TestReferralLinks.cs @@ -1,57 +1,54 @@ -using NUnit.Framework; +namespace Markdig.Tests; -namespace Markdig.Tests +[TestFixture] +public class TestReferralLinks { - [TestFixture] - public class TestReferralLinks + [Test] + public void TestLinksWithNoFollowRel() { - [Test] - public void TestLinksWithNoFollowRel() - { - var markdown = "[world](http://example.com)"; - var expected = "nofollow"; + var markdown = "[world](http://example.com)"; + var expected = "nofollow"; #pragma warning disable 0618 - var pipeline = new MarkdownPipelineBuilder() - .UseNoFollowLinks() + var pipeline = new MarkdownPipelineBuilder() + .UseNoFollowLinks() #pragma warning restore 0618 - .Build(); - var html = Markdown.ToHtml(markdown, pipeline); - - Assert.That(html, Contains.Substring($"rel=\"{expected}\"")); - } - - [Test] - [TestCase(new[] { "nofollow" }, "nofollow")] - [TestCase(new[] { "noopener" }, "noopener")] - [TestCase(new[] { "nofollow", "noopener"}, "nofollow noopener")] - public void TestLinksWithCustomRel(string[] rels, string expected) - { - var markdown = "[world](http://example.com)"; - - var pipeline = new MarkdownPipelineBuilder() - .UseReferralLinks(rels) - .Build(); - var html = Markdown.ToHtml(markdown, pipeline); - - Assert.That(html, Contains.Substring($"rel=\"{expected}\"")); - } - - [Test] - [TestCase(new[] { "noopener" }, "noopener")] - [TestCase(new[] { "nofollow" }, "nofollow")] - [TestCase(new[] { "nofollow", "noopener" }, "nofollow noopener")] - public void TestAutoLinksWithCustomRel(string[] rels, string expected) - { - var markdown = "http://example.com"; - - var pipeline = new MarkdownPipelineBuilder() - .UseAutoLinks() - .UseReferralLinks(rels) - .Build(); - var html = Markdown.ToHtml(markdown, pipeline); - - Assert.That(html, Contains.Substring($"rel=\"{expected}\"")); - } + .Build(); + var html = Markdown.ToHtml(markdown, pipeline); + + Assert.That(html, Contains.Substring($"rel=\"{expected}\"")); + } + + [Test] + [TestCase(new[] { "nofollow" }, "nofollow")] + [TestCase(new[] { "noopener" }, "noopener")] + [TestCase(new[] { "nofollow", "noopener"}, "nofollow noopener")] + public void TestLinksWithCustomRel(string[] rels, string expected) + { + var markdown = "[world](http://example.com)"; + + var pipeline = new MarkdownPipelineBuilder() + .UseReferralLinks(rels) + .Build(); + var html = Markdown.ToHtml(markdown, pipeline); + + Assert.That(html, Contains.Substring($"rel=\"{expected}\"")); + } + + [Test] + [TestCase(new[] { "noopener" }, "noopener")] + [TestCase(new[] { "nofollow" }, "nofollow")] + [TestCase(new[] { "nofollow", "noopener" }, "nofollow noopener")] + public void TestAutoLinksWithCustomRel(string[] rels, string expected) + { + var markdown = "http://example.com"; + + var pipeline = new MarkdownPipelineBuilder() + .UseAutoLinks() + .UseReferralLinks(rels) + .Build(); + var html = Markdown.ToHtml(markdown, pipeline); + + Assert.That(html, Contains.Substring($"rel=\"{expected}\"")); } } diff --git a/src/Markdig.Tests/TestRelativeUrlReplacement.cs b/src/Markdig.Tests/TestRelativeUrlReplacement.cs index edf9b83f3..c13ef18d0 100644 --- a/src/Markdig.Tests/TestRelativeUrlReplacement.cs +++ b/src/Markdig.Tests/TestRelativeUrlReplacement.cs @@ -1,48 +1,44 @@ -using System; -using System.IO; using Markdig.Parsers; using Markdig.Renderers; -using NUnit.Framework; -namespace Markdig.Tests +namespace Markdig.Tests; + +public class TestRelativeUrlReplacement { - public class TestRelativeUrlReplacement + [Test] + public void ReplacesRelativeLinks() { - [Test] - public void ReplacesRelativeLinks() - { - TestSpec("https://example.com", "Link: [hello](/relative.jpg)", "https://example.com/relative.jpg"); - TestSpec("https://example.com", "Link: [hello](relative.jpg)", "https://example.com/relative.jpg"); - TestSpec("https://example.com/", "Link: [hello](/relative.jpg?a=b)", "https://example.com/relative.jpg?a=b"); - TestSpec("https://example.com/", "Link: [hello](relative.jpg#x)", "https://example.com/relative.jpg#x"); - TestSpec(null, "Link: [hello](relative.jpg)", "relative.jpg"); - TestSpec(null, "Link: [hello](/relative.jpg)", "/relative.jpg"); - TestSpec("https://example.com", "Link: [hello](/relative.jpg)", "https://example.com/relative.jpg"); - } + TestSpec("https://example.com", "Link: [hello](/relative.jpg)", "https://example.com/relative.jpg"); + TestSpec("https://example.com", "Link: [hello](relative.jpg)", "https://example.com/relative.jpg"); + TestSpec("https://example.com/", "Link: [hello](/relative.jpg?a=b)", "https://example.com/relative.jpg?a=b"); + TestSpec("https://example.com/", "Link: [hello](relative.jpg#x)", "https://example.com/relative.jpg#x"); + TestSpec(null, "Link: [hello](relative.jpg)", "relative.jpg"); + TestSpec(null, "Link: [hello](/relative.jpg)", "/relative.jpg"); + TestSpec("https://example.com", "Link: [hello](/relative.jpg)", "https://example.com/relative.jpg"); + } - [Test] - public void ReplacesRelativeImageSources() - { - TestSpec("https://example.com", "Image: ![alt text](/image.jpg)", "https://example.com/image.jpg"); - TestSpec("https://example.com", "Image: ![alt text](image.jpg \"title\")", "https://example.com/image.jpg"); - TestSpec(null, "Image: ![alt text](/image.jpg)", "/image.jpg"); - } + [Test] + public void ReplacesRelativeImageSources() + { + TestSpec("https://example.com", "Image: ![alt text](/image.jpg)", "https://example.com/image.jpg"); + TestSpec("https://example.com", "Image: ![alt text](image.jpg \"title\")", "https://example.com/image.jpg"); + TestSpec(null, "Image: ![alt text](/image.jpg)", "/image.jpg"); + } - public static void TestSpec(string baseUrl, string markdown, string expectedLink) - { - var pipeline = new MarkdownPipelineBuilder().Build(); + public static void TestSpec(string baseUrl, string markdown, string expectedLink) + { + var pipeline = new MarkdownPipelineBuilder().Build(); - var writer = new StringWriter(); - var renderer = new HtmlRenderer(writer); - if (baseUrl != null) - renderer.BaseUrl = new Uri(baseUrl); - pipeline.Setup(renderer); + var writer = new StringWriter(); + var renderer = new HtmlRenderer(writer); + if (baseUrl != null) + renderer.BaseUrl = new Uri(baseUrl); + pipeline.Setup(renderer); - var document = MarkdownParser.Parse(markdown, pipeline); - renderer.Render(document); - writer.Flush(); + var document = MarkdownParser.Parse(markdown, pipeline); + renderer.Render(document); + writer.Flush(); - Assert.That(writer.ToString(), Contains.Substring("=\"" + expectedLink + "\"")); - } + Assert.That(writer.ToString(), Contains.Substring("=\"" + expectedLink + "\"")); } } \ No newline at end of file diff --git a/src/Markdig.Tests/TestRoundtrip.cs b/src/Markdig.Tests/TestRoundtrip.cs index 8347eae1c..24868b6bf 100644 --- a/src/Markdig.Tests/TestRoundtrip.cs +++ b/src/Markdig.Tests/TestRoundtrip.cs @@ -1,32 +1,29 @@ using Markdig.Renderers.Roundtrip; using Markdig.Syntax; -using NUnit.Framework; -using System.IO; -namespace Markdig.Tests +namespace Markdig.Tests; + +internal static class TestRoundtrip { - internal static class TestRoundtrip + internal static void TestSpec(string markdownText, string expected, string extensions, string context = null) { - internal static void TestSpec(string markdownText, string expected, string extensions, string context = null) - { - RoundTrip(markdownText, context); - } + RoundTrip(markdownText, context); + } - internal static void RoundTrip(string markdown, string context = null) - { - var pipelineBuilder = new MarkdownPipelineBuilder(); - pipelineBuilder.EnableTrackTrivia(); - pipelineBuilder.UseYamlFrontMatter(); - MarkdownPipeline pipeline = pipelineBuilder.Build(); - MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); - var sw = new StringWriter(); - var nr = new RoundtripRenderer(sw); - pipeline.Setup(nr); + internal static void RoundTrip(string markdown, string context = null) + { + var pipelineBuilder = new MarkdownPipelineBuilder(); + pipelineBuilder.EnableTrackTrivia(); + pipelineBuilder.UseYamlFrontMatter(); + MarkdownPipeline pipeline = pipelineBuilder.Build(); + MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); + var sw = new StringWriter(); + var nr = new RoundtripRenderer(sw); + pipeline.Setup(nr); - nr.Write(markdownDocument); + nr.Write(markdownDocument); - var result = sw.ToString(); - TestParser.PrintAssertExpected("", result, markdown, context); - } + var result = sw.ToString(); + TestParser.PrintAssertExpected("", result, markdown, context); } } diff --git a/src/Markdig.Tests/TestSmartyPants.cs b/src/Markdig.Tests/TestSmartyPants.cs index 77d82e4bf..27ff87c9a 100644 --- a/src/Markdig.Tests/TestSmartyPants.cs +++ b/src/Markdig.Tests/TestSmartyPants.cs @@ -1,36 +1,34 @@ using Markdig.Extensions.SmartyPants; -using NUnit.Framework; -namespace Markdig.Tests +namespace Markdig.Tests; + +public class TestSmartyPants { - public class TestSmartyPants + [Test] + public void MappingCanBeReconfigured() { - [Test] - public void MappingCanBeReconfigured() - { - SmartyPantOptions options = new SmartyPantOptions(); - options.Mapping[SmartyPantType.LeftAngleQuote] = "foo"; - options.Mapping[SmartyPantType.RightAngleQuote] = "bar"; + var options = new SmartyPantOptions(); + options.Mapping[SmartyPantType.LeftAngleQuote] = "foo"; + options.Mapping[SmartyPantType.RightAngleQuote] = "bar"; - var pipeline = new MarkdownPipelineBuilder() - .UseSmartyPants(options) - .Build(); + var pipeline = new MarkdownPipelineBuilder() + .UseSmartyPants(options) + .Build(); - TestParser.TestSpec("<>", "

      footestbar

      ", pipeline); - } + TestParser.TestSpec("<>", "

      footestbar

      ", pipeline); + } - [Test] - public void MappingCanBeReconfigured_HandlesRemovedMappings() - { - SmartyPantOptions options = new SmartyPantOptions(); - options.Mapping.Remove(SmartyPantType.LeftAngleQuote); - options.Mapping.Remove(SmartyPantType.RightAngleQuote); + [Test] + public void MappingCanBeReconfigured_HandlesRemovedMappings() + { + var options = new SmartyPantOptions(); + options.Mapping.Remove(SmartyPantType.LeftAngleQuote); + options.Mapping.Remove(SmartyPantType.RightAngleQuote); - var pipeline = new MarkdownPipelineBuilder() - .UseSmartyPants(options) - .Build(); + var pipeline = new MarkdownPipelineBuilder() + .UseSmartyPants(options) + .Build(); - TestParser.TestSpec("<>", "

      «test»

      ", pipeline); - } + TestParser.TestSpec("<>", "

      «test»

      ", pipeline); } } diff --git a/src/Markdig.Tests/TestSourcePosition.cs b/src/Markdig.Tests/TestSourcePosition.cs index 55bd8199c..5f12beef2 100644 --- a/src/Markdig.Tests/TestSourcePosition.cs +++ b/src/Markdig.Tests/TestSourcePosition.cs @@ -2,90 +2,87 @@ // 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.Collections.Generic; -using System.Linq; using System.Text; + using Markdig.Extensions.Footnotes; using Markdig.Renderers.Html; using Markdig.Syntax; using Markdig.Syntax.Inlines; -using NUnit.Framework; -namespace Markdig.Tests +namespace Markdig.Tests; + +/// +/// Test the precise source location of all Markdown elements, including extensions +/// +[TestFixture] +public class TestSourcePosition { - /// - /// Test the precise source location of all Markdown elements, including extensions - /// - [TestFixture] - public class TestSourcePosition - { - [Test] - public void TestParagraph() - { - Check("0123456789", @" + [Test] + public void TestParagraph() + { + Check("0123456789", @" paragraph ( 0, 0) 0-9 literal ( 0, 0) 0-9 "); - } + } - [Test] - public void TestParagraphAndNewLine() - { - Check("0123456789\n0123456789", @" + [Test] + public void TestParagraphAndNewLine() + { + Check("0123456789\n0123456789", @" paragraph ( 0, 0) 0-20 literal ( 0, 0) 0-9 linebreak ( 0,10) 10-10 literal ( 1, 0) 11-20 "); - Check("0123456789\r\n0123456789", @" + Check("0123456789\r\n0123456789", @" paragraph ( 0, 0) 0-21 literal ( 0, 0) 0-9 linebreak ( 0,10) 10-10 literal ( 1, 0) 12-21 "); - } + } - [Test] - public void TestParagraphNewLineAndSpaces() - { - // 0123 45678 - Check("012\n 345", @" + [Test] + public void TestParagraphNewLineAndSpaces() + { + // 0123 45678 + Check("012\n 345", @" paragraph ( 0, 0) 0-8 literal ( 0, 0) 0-2 linebreak ( 0, 3) 3-3 literal ( 1, 2) 6-8 "); - } + } - [Test] - public void TestParagraph2() - { - Check("0123456789\n\n0123456789", @" + [Test] + public void TestParagraph2() + { + Check("0123456789\n\n0123456789", @" paragraph ( 0, 0) 0-9 literal ( 0, 0) 0-9 paragraph ( 2, 0) 12-21 literal ( 2, 0) 12-21 "); - } + } - [Test] - public void TestEmphasis() - { - Check("012**3456789**", @" + [Test] + public void TestEmphasis() + { + Check("012**3456789**", @" paragraph ( 0, 0) 0-13 literal ( 0, 0) 0-2 emphasis ( 0, 3) 3-13 literal ( 0, 5) 5-11 "); - } + } - [Test] - public void TestEmphasis2() - { - // 01234567 - Check("01*2**3*", @" + [Test] + public void TestEmphasis2() + { + // 01234567 + Check("01*2**3*", @" paragraph ( 0, 0) 0-7 literal ( 0, 0) 0-1 emphasis ( 0, 2) 2-7 @@ -93,13 +90,13 @@ public void TestEmphasis2() literal ( 0, 4) 4-5 literal ( 0, 6) 6-6 "); - } + } - [Test] - public void TestEmphasis3() - { - // 0123456789 - Check("01**2***3*", @" + [Test] + public void TestEmphasis3() + { + // 0123456789 + Check("01**2***3*", @" paragraph ( 0, 0) 0-9 literal ( 0, 0) 0-1 emphasis ( 0, 2) 2-6 @@ -107,286 +104,286 @@ public void TestEmphasis3() emphasis ( 0, 7) 7-9 literal ( 0, 8) 8-8 "); - } + } - [Test] - public void TestEmphasisFalse() - { - Check("0123456789**0123", @" + [Test] + public void TestEmphasisFalse() + { + Check("0123456789**0123", @" paragraph ( 0, 0) 0-15 literal ( 0, 0) 0-9 literal ( 0,10) 10-11 literal ( 0,12) 12-15 "); - } + } - [Test] - public void TestHeading() - { - // 012345 - Check("# 2345", @" + [Test] + public void TestHeading() + { + // 012345 + Check("# 2345", @" heading ( 0, 0) 0-5 literal ( 0, 2) 2-5 "); - } + } - [Test] - public void TestHeadingWithEmphasis() - { - // 0123456789 - Check("# 23**45**", @" + [Test] + public void TestHeadingWithEmphasis() + { + // 0123456789 + Check("# 23**45**", @" heading ( 0, 0) 0-9 literal ( 0, 2) 2-3 emphasis ( 0, 4) 4-9 literal ( 0, 6) 6-7 "); - } + } - [Test] - public void TestFootnoteLinkReferenceDefinition() - { - // 01 2 345678 - var footnote = Markdown.Parse("0\n\n [^1]:", new MarkdownPipelineBuilder().UsePreciseSourceLocation().UseFootnotes().Build()).Descendants().FirstOrDefault(); - Assert.NotNull(footnote); + [Test] + public void TestFootnoteLinkReferenceDefinition() + { + // 01 2 345678 + var footnote = Markdown.Parse("0\n\n [^1]:", new MarkdownPipelineBuilder().UsePreciseSourceLocation().UseFootnotes().Build()).Descendants().FirstOrDefault(); + Assert.NotNull(footnote); - Assert.AreEqual(2, footnote.Line); - Assert.AreEqual(new SourceSpan(4, 7), footnote.Span); - Assert.AreEqual(new SourceSpan(5, 6), footnote.LabelSpan); - } + Assert.AreEqual(2, footnote.Line); + Assert.AreEqual(new SourceSpan(4, 7), footnote.Span); + Assert.AreEqual(new SourceSpan(5, 6), footnote.LabelSpan); + } - [Test] - public void TestLinkReferenceDefinition1() - { - // 0 1 - // 0123456789012345 - var link = Markdown.Parse("[234]: /56 'yo' ", new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants().FirstOrDefault(); - Assert.NotNull(link); - - Assert.AreEqual(0, link.Line); - Assert.AreEqual(new SourceSpan(0, 14), link.Span); - Assert.AreEqual(new SourceSpan(1, 3), link.LabelSpan); - Assert.AreEqual(new SourceSpan(7, 9), link.UrlSpan); - Assert.AreEqual(new SourceSpan(11, 14), link.TitleSpan); - } + [Test] + public void TestLinkReferenceDefinition1() + { + // 0 1 + // 0123456789012345 + var link = Markdown.Parse("[234]: /56 'yo' ", new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants().FirstOrDefault(); + Assert.NotNull(link); + + Assert.AreEqual(0, link.Line); + Assert.AreEqual(new SourceSpan(0, 14), link.Span); + Assert.AreEqual(new SourceSpan(1, 3), link.LabelSpan); + Assert.AreEqual(new SourceSpan(7, 9), link.UrlSpan); + Assert.AreEqual(new SourceSpan(11, 14), link.TitleSpan); + } - [Test] - public void TestLinkReferenceDefinition2() - { - // 0 1 - // 01 2 34567890123456789 - var link = Markdown.Parse("0\n\n [234]: /56 'yo' ", new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants().FirstOrDefault(); - Assert.NotNull(link); - - Assert.AreEqual(2, link.Line); - Assert.AreEqual(new SourceSpan(4, 18), link.Span); - Assert.AreEqual(new SourceSpan(5, 7), link.LabelSpan); - Assert.AreEqual(new SourceSpan(11, 13), link.UrlSpan); - Assert.AreEqual(new SourceSpan(15, 18), link.TitleSpan); - } + [Test] + public void TestLinkReferenceDefinition2() + { + // 0 1 + // 01 2 34567890123456789 + var link = Markdown.Parse("0\n\n [234]: /56 'yo' ", new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants().FirstOrDefault(); + Assert.NotNull(link); + + Assert.AreEqual(2, link.Line); + Assert.AreEqual(new SourceSpan(4, 18), link.Span); + Assert.AreEqual(new SourceSpan(5, 7), link.LabelSpan); + Assert.AreEqual(new SourceSpan(11, 13), link.UrlSpan); + Assert.AreEqual(new SourceSpan(15, 18), link.TitleSpan); + } - [Test] - public void TestCodeSpan() - { - // 012345678 - Check("0123`456`", @" + [Test] + public void TestCodeSpan() + { + // 012345678 + Check("0123`456`", @" paragraph ( 0, 0) 0-8 literal ( 0, 0) 0-3 code ( 0, 4) 4-8 "); - } + } - [Test] - public void TestLink() - { - // 0123456789 - Check("012[45](#)", @" + [Test] + public void TestLink() + { + // 0123456789 + Check("012[45](#)", @" paragraph ( 0, 0) 0-9 literal ( 0, 0) 0-2 link ( 0, 3) 3-9 literal ( 0, 4) 4-5 "); - } + } - [Test] - public void TestLinkParts1() - { - // 0 1 - // 01 2 3456789012345 - var link = Markdown.Parse("0\n\n01 [234](/56)", new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants().FirstOrDefault(); - Assert.NotNull(link); - - Assert.AreEqual(new SourceSpan(7, 9), link.LabelSpan); - Assert.AreEqual(new SourceSpan(12, 14), link.UrlSpan); - Assert.AreEqual(SourceSpan.Empty, link.TitleSpan); - } + [Test] + public void TestLinkParts1() + { + // 0 1 + // 01 2 3456789012345 + var link = Markdown.Parse("0\n\n01 [234](/56)", new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants().FirstOrDefault(); + Assert.NotNull(link); + + Assert.AreEqual(new SourceSpan(7, 9), link.LabelSpan); + Assert.AreEqual(new SourceSpan(12, 14), link.UrlSpan); + Assert.AreEqual(SourceSpan.Empty, link.TitleSpan); + } - [Test] - public void TestLinkParts2() - { - // 0 1 - // 01 2 34567890123456789 - var link = Markdown.Parse("0\n\n01 [234](/56 'yo')", new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants().FirstOrDefault(); - Assert.NotNull(link); - - Assert.AreEqual(new SourceSpan(7, 9), link.LabelSpan); - Assert.AreEqual(new SourceSpan(12, 14), link.UrlSpan); - Assert.AreEqual(new SourceSpan(16, 19), link.TitleSpan); - } + [Test] + public void TestLinkParts2() + { + // 0 1 + // 01 2 34567890123456789 + var link = Markdown.Parse("0\n\n01 [234](/56 'yo')", new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants().FirstOrDefault(); + Assert.NotNull(link); + + Assert.AreEqual(new SourceSpan(7, 9), link.LabelSpan); + Assert.AreEqual(new SourceSpan(12, 14), link.UrlSpan); + Assert.AreEqual(new SourceSpan(16, 19), link.TitleSpan); + } - [Test] - public void TestLinkParts3() - { - // 0 1 - // 01 2 3456789012345 - var link = Markdown.Parse("0\n\n01![234](/56)", new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants().FirstOrDefault(); - Assert.NotNull(link); - - Assert.AreEqual(new SourceSpan(5, 15), link.Span); - Assert.AreEqual(new SourceSpan(7, 9), link.LabelSpan); - Assert.AreEqual(new SourceSpan(12, 14), link.UrlSpan); - Assert.AreEqual(SourceSpan.Empty, link.TitleSpan); - } + [Test] + public void TestLinkParts3() + { + // 0 1 + // 01 2 3456789012345 + var link = Markdown.Parse("0\n\n01![234](/56)", new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants().FirstOrDefault(); + Assert.NotNull(link); + + Assert.AreEqual(new SourceSpan(5, 15), link.Span); + Assert.AreEqual(new SourceSpan(7, 9), link.LabelSpan); + Assert.AreEqual(new SourceSpan(12, 14), link.UrlSpan); + Assert.AreEqual(SourceSpan.Empty, link.TitleSpan); + } - [Test] - public void TestAutolinkInline() - { - // 0123456789ABCD - Check("01", @" + [Test] + public void TestAutolinkInline() + { + // 0123456789ABCD + Check("01", @" paragraph ( 0, 0) 0-13 literal ( 0, 0) 0-1 autolink ( 0, 2) 2-13 "); - } + } - [Test] - public void TestFencedCodeBlock() - { - // 012 3456 78 9ABC - Check("01\n```\n3\n```\n", @" + [Test] + public void TestFencedCodeBlock() + { + // 012 3456 78 9ABC + Check("01\n```\n3\n```\n", @" paragraph ( 0, 0) 0-1 literal ( 0, 0) 0-1 fencedcode ( 1, 0) 3-11 "); - } + } - [Test] - public void TestHtmlBlock() - { - // 012345 67 89ABCDE F 0 - Check("
      \n0\n
      \n\n1", @" + [Test] + public void TestHtmlBlock() + { + // 012345 67 89ABCDE F 0 + Check("
      \n0\n
      \n\n1", @" html ( 0, 0) 0-13 paragraph ( 4, 0) 16-16 literal ( 4, 0) 16-16 "); - } + } - [Test] - public void TestHtmlBlock1() - { - // 0 1 - // 01 2 345678901 23 - Check("0\n\n\n1\n", @" + [Test] + public void TestHtmlBlock1() + { + // 0 1 + // 01 2 345678901 23 + Check("0\n\n\n1\n", @" paragraph ( 0, 0) 0-0 literal ( 0, 0) 0-0 html ( 2, 0) 3-10 paragraph ( 3, 0) 12-12 literal ( 3, 0) 12-12 "); - } + } - [Test] - public void TestHtmlComment() - { - // 0 1 2 - // 012345678901 234567890 1234 - Check("# 012345678\n\n123\n", @" + [Test] + public void TestHtmlComment() + { + // 0 1 2 + // 012345678901 234567890 1234 + Check("# 012345678\n\n123\n", @" heading ( 0, 0) 0-10 literal ( 0, 2) 2-10 html ( 1, 0) 12-19 paragraph ( 2, 0) 21-23 literal ( 2, 0) 21-23 "); - } + } - [Test] - public void TestHtmlInline() - { - // 0123456789 - Check("014", @" + [Test] + public void TestHtmlInline() + { + // 0123456789 + Check("014", @" paragraph ( 0, 0) 0-9 literal ( 0, 0) 0-1 html ( 0, 2) 2-4 literal ( 0, 5) 5-5 html ( 0, 6) 6-9 "); - } + } - [Test] - public void TestHtmlInline1() - { - // 0 - // 0123456789 - Check("01", @" + [Test] + public void TestHtmlInline1() + { + // 0 + // 0123456789 + Check("01", @" paragraph ( 0, 0) 0-9 literal ( 0, 0) 0-0 html ( 0, 1) 1-8 literal ( 0, 9) 9-9 "); - } + } - [Test] - public void TestThematicBreak() - { - // 0123 4567 - Check("---\n---\n", @" + [Test] + public void TestThematicBreak() + { + // 0123 4567 + Check("---\n---\n", @" thematicbreak ( 0, 0) 0-2 thematicbreak ( 1, 0) 4-6 "); - } + } - [Test] - public void TestQuoteBlock() - { - // 0123456 - Check("> 2345\n", @" + [Test] + public void TestQuoteBlock() + { + // 0123456 + Check("> 2345\n", @" quote ( 0, 0) 0-5 paragraph ( 0, 2) 2-5 literal ( 0, 2) 2-5 "); - } + } - [Test] - public void TestQuoteBlockWithLines() - { - // 01234 56789A - Check("> 01\n> 23\n", @" + [Test] + public void TestQuoteBlockWithLines() + { + // 01234 56789A + Check("> 01\n> 23\n", @" quote ( 0, 0) 0-9 paragraph ( 0, 2) 2-9 literal ( 0, 2) 2-3 linebreak ( 0, 4) 4-4 literal ( 1, 3) 8-9 "); - } + } - [Test] - public void TestQuoteBlockWithLazyContinuation() - { - // 01234 56 - Check("> 01\n23\n", @" + [Test] + public void TestQuoteBlockWithLazyContinuation() + { + // 01234 56 + Check("> 01\n23\n", @" quote ( 0, 0) 0-6 paragraph ( 0, 2) 2-6 literal ( 0, 2) 2-3 linebreak ( 0, 4) 4-4 literal ( 1, 0) 5-6 "); - } + } - [Test] - public void TestListBlock() - { - // 0123 4567 - Check("- 0\n- 1\n", @" + [Test] + public void TestListBlock() + { + // 0123 4567 + Check("- 0\n- 1\n", @" list ( 0, 0) 0-6 listitem ( 0, 0) 0-2 paragraph ( 0, 2) 2-2 @@ -395,72 +392,72 @@ public void TestListBlock() paragraph ( 1, 2) 6-6 literal ( 1, 2) 6-6 "); - } + } - [Test] - public void TestListBlock2() - { - string test = @" + [Test] + public void TestListBlock2() + { + string test = @" 1. Foo 9. Bar 5. Foo 6. Bar 987123. FooBar"; - test = test.Replace("\r\n", "\n"); - var list = Markdown.Parse(test, new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants().FirstOrDefault(); - Assert.NotNull(list); - - Assert.AreEqual(1, list.Line); - Assert.True(list.IsOrdered); - List items = list.Cast().ToList(); - Assert.AreEqual(5, items.Count); - - // Test orders - Assert.AreEqual(1, items[0].Order); - Assert.AreEqual(9, items[1].Order); - Assert.AreEqual(5, items[2].Order); - Assert.AreEqual(6, items[3].Order); - Assert.AreEqual(987123, items[4].Order); - - // Test positions - for (int i = 0; i < 4; i++) - { - Assert.AreEqual(i + 1, items[i].Line); - Assert.AreEqual(1 + (i * 7), items[i].Span.Start); - Assert.AreEqual(6, items[i].Span.Length); - } - Assert.AreEqual(5, items[4].Line); - Assert.AreEqual(new SourceSpan(29, 42), items[4].Span); - } + test = test.Replace("\r\n", "\n"); + var list = Markdown.Parse(test, new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants().FirstOrDefault(); + Assert.NotNull(list); + + Assert.AreEqual(1, list.Line); + Assert.True(list.IsOrdered); + List items = list.Cast().ToList(); + Assert.AreEqual(5, items.Count); + + // Test orders + Assert.AreEqual(1, items[0].Order); + Assert.AreEqual(9, items[1].Order); + Assert.AreEqual(5, items[2].Order); + Assert.AreEqual(6, items[3].Order); + Assert.AreEqual(987123, items[4].Order); + + // Test positions + for (int i = 0; i < 4; i++) + { + Assert.AreEqual(i + 1, items[i].Line); + Assert.AreEqual(1 + (i * 7), items[i].Span.Start); + Assert.AreEqual(6, items[i].Span.Length); + } + Assert.AreEqual(5, items[4].Line); + Assert.AreEqual(new SourceSpan(29, 42), items[4].Span); + } - [Test] - public void TestEscapeInline() - { - // 0123 - Check(@"\-\)", @" + [Test] + public void TestEscapeInline() + { + // 0123 + Check(@"\-\)", @" paragraph ( 0, 0) 0-3 literal ( 0, 0) 0-1 literal ( 0, 2) 2-3 "); - } + } - [Test] - public void TestHtmlEntityInline() - { - // 01 23456789 - Check("0\n  1", @" + [Test] + public void TestHtmlEntityInline() + { + // 01 23456789 + Check("0\n  1", @" paragraph ( 0, 0) 0-9 literal ( 0, 0) 0-0 linebreak ( 0, 1) 1-1 htmlentity ( 1, 0) 2-7 literal ( 1, 6) 8-9 "); - } + } - [Test] - public void TestAbbreviations() - { - Check("*[HTML]: Hypertext Markup Language\r\n\r\nLater in a text we are using HTML and it becomes an abbr tag HTML", @" + [Test] + public void TestAbbreviations() + { + Check("*[HTML]: Hypertext Markup Language\r\n\r\nLater in a text we are using HTML and it becomes an abbr tag HTML", @" paragraph ( 2, 0) 38-102 container ( 2, 0) 38-102 literal ( 2, 0) 38-66 @@ -468,25 +465,25 @@ public void TestAbbreviations() literal ( 2,33) 71-98 abbreviation ( 2,61) 99-102 ", "abbreviations"); - } + } - [Test] - public void TestCitation() - { - // 0123 4 567 8 - Check("01 \"\"23\"\"", @" + [Test] + public void TestCitation() + { + // 0123 4 567 8 + Check("01 \"\"23\"\"", @" paragraph ( 0, 0) 0-8 literal ( 0, 0) 0-2 emphasis ( 0, 3) 3-8 literal ( 0, 5) 5-6 ", "citations"); - } + } - [Test] - public void TestCustomContainer() - { - // 01 2345 678 9ABC DEF - Check("0\n:::\n23\n:::\n45\n", @" + [Test] + public void TestCustomContainer() + { + // 01 2345 678 9ABC DEF + Check("0\n:::\n23\n:::\n45\n", @" paragraph ( 0, 0) 0-0 literal ( 0, 0) 0-0 customcontainer ( 1, 0) 2-11 @@ -495,13 +492,13 @@ public void TestCustomContainer() paragraph ( 4, 0) 13-14 literal ( 4, 0) 13-14 ", "customcontainers"); - } + } - [Test] - public void TestDefinitionList() - { - // 012 3456789A - Check("a0\n: 1234", @" + [Test] + public void TestDefinitionList() + { + // 012 3456789A + Check("a0\n: 1234", @" definitionlist ( 0, 0) 0-10 definitionitem ( 1, 0) 3-10 definitionterm ( 0, 0) 0-1 @@ -509,13 +506,13 @@ public void TestDefinitionList() paragraph ( 1, 4) 7-10 literal ( 1, 4) 7-10 ", "definitionlists"); - } + } - [Test] - public void TestDefinitionList2() - { - // 012 3456789AB CDEF01234 - Check("a0\n: 1234\n: 5678", @" + [Test] + public void TestDefinitionList2() + { + // 012 3456789AB CDEF01234 + Check("a0\n: 1234\n: 5678", @" definitionlist ( 0, 0) 0-20 definitionitem ( 1, 0) 3-10 definitionterm ( 0, 0) 0-1 @@ -526,50 +523,50 @@ public void TestDefinitionList2() paragraph ( 2, 5) 17-20 literal ( 2, 5) 17-20 ", "definitionlists"); - } + } - [Test] - public void TestEmoji() - { - // 01 2345 - Check("0\n :)\n", @" + [Test] + public void TestEmoji() + { + // 01 2345 + Check("0\n :)\n", @" paragraph ( 0, 0) 0-4 literal ( 0, 0) 0-0 linebreak ( 0, 1) 1-1 emoji ( 1, 1) 3-4 ", "emojis"); - } + } - [Test] - public void TestEmphasisExtra() - { - // 0123456 - Check("0 ~~1~~", @" + [Test] + public void TestEmphasisExtra() + { + // 0123456 + Check("0 ~~1~~", @" paragraph ( 0, 0) 0-6 literal ( 0, 0) 0-1 emphasis ( 0, 2) 2-6 literal ( 0, 4) 4-4 ", "emphasisextras"); - } + } - [Test] - public void TestFigures() - { - // 01 2345 67 89AB - Check("0\n^^^\n0\n^^^\n", @" + [Test] + public void TestFigures() + { + // 01 2345 67 89AB + Check("0\n^^^\n0\n^^^\n", @" paragraph ( 0, 0) 0-0 literal ( 0, 0) 0-0 figure ( 1, 0) 2-10 paragraph ( 2, 0) 6-6 literal ( 2, 0) 6-6 ", "figures"); - } + } - [Test] - public void TestFiguresCaption1() - { - // 01 234567 89 ABCD - Check("0\n^^^ab\n0\n^^^\n", @" + [Test] + public void TestFiguresCaption1() + { + // 01 234567 89 ABCD + Check("0\n^^^ab\n0\n^^^\n", @" paragraph ( 0, 0) 0-0 literal ( 0, 0) 0-0 figure ( 1, 0) 2-12 @@ -578,13 +575,13 @@ public void TestFiguresCaption1() paragraph ( 2, 0) 8-8 literal ( 2, 0) 8-8 ", "figures"); - } + } - [Test] - public void TestFiguresCaption2() - { - // 01 2345 67 89ABCD - Check("0\n^^^\n0\n^^^ab\n", @" + [Test] + public void TestFiguresCaption2() + { + // 01 2345 67 89ABCD + Check("0\n^^^\n0\n^^^ab\n", @" paragraph ( 0, 0) 0-0 literal ( 0, 0) 0-0 figure ( 1, 0) 2-12 @@ -593,13 +590,13 @@ public void TestFiguresCaption2() figurecaption ( 3, 3) 11-12 literal ( 3, 3) 11-12 ", "figures"); - } + } - [Test] - public void TestFooters() - { - // 01 234567 89ABCD - Check("0\n^^ 12\n^^ 34\n", @" + [Test] + public void TestFooters() + { + // 01 234567 89ABCD + Check("0\n^^ 12\n^^ 34\n", @" paragraph ( 0, 0) 0-0 literal ( 0, 0) 0-0 footer ( 1, 0) 2-12 @@ -608,36 +605,36 @@ public void TestFooters() linebreak ( 1, 5) 7-7 literal ( 2, 3) 11-12 ", "footers"); - } + } - [Test] - public void TestAttributes() - { - // 0123456789 - Check("0123{#456}", @" + [Test] + public void TestAttributes() + { + // 0123456789 + Check("0123{#456}", @" paragraph ( 0, 0) 0-9 attributes ( 0, 4) 4-9 literal ( 0, 0) 0-3 ", "attributes"); - } + } - [Test] - public void TestAttributesForHeading() - { - // 0123456789ABC - Check("# 01 {#456}", @" + [Test] + public void TestAttributesForHeading() + { + // 0123456789ABC + Check("# 01 {#456}", @" heading ( 0, 0) 0-4 attributes ( 0, 5) 5-10 literal ( 0, 2) 2-3 ", "attributes"); - } + } - [Test] - public void TestMathematicsInline() - { - // 01 23456789AB - Check("0\n012 $abcd$", @" + [Test] + public void TestMathematicsInline() + { + // 01 23456789AB + Check("0\n012 $abcd$", @" paragraph ( 0, 0) 0-11 literal ( 0, 0) 0-0 linebreak ( 0, 1) 1-1 @@ -645,14 +642,14 @@ public void TestMathematicsInline() math ( 1, 4) 6-11 attributes ( 0, 0) 0--1 ", "mathematics"); - } + } - [Test] - public void TestSmartyPants() - { - // 01234567 - // 01 23456789 - Check("0\n2 <<45>>", @" + [Test] + public void TestSmartyPants() + { + // 01234567 + // 01 23456789 + Check("0\n2 <<45>>", @" paragraph ( 0, 0) 0-9 literal ( 0, 0) 0-0 linebreak ( 0, 1) 1-1 @@ -661,14 +658,14 @@ public void TestSmartyPants() literal ( 1, 4) 6-7 smartypant ( 1, 6) 8-9 ", "smartypants"); - } + } - [Test] - public void TestSmartyPantsUnbalanced() - { - // 012345 - // 01 234567 - Check("0\n2 <<45", @" + [Test] + public void TestSmartyPantsUnbalanced() + { + // 012345 + // 01 234567 + Check("0\n2 <<45", @" paragraph ( 0, 0) 0-7 literal ( 0, 0) 0-0 linebreak ( 0, 1) 1-1 @@ -676,13 +673,13 @@ public void TestSmartyPantsUnbalanced() literal ( 1, 2) 4-5 literal ( 1, 4) 6-7 ", "smartypants"); - } + } - [Test] - public void TestPipeTable() - { - // 0123 4567 89AB - Check("a|b\n-|-\n0|1\n", @" + [Test] + public void TestPipeTable() + { + // 0123 4567 89AB + Check("a|b\n-|-\n0|1\n", @" table ( 0, 0) 0-10 tablerow ( 0, 0) 0-2 tablecell ( 0, 0) 0-0 @@ -699,13 +696,13 @@ public void TestPipeTable() paragraph ( 2, 2) 10-10 literal ( 2, 2) 10-10 ", "pipetables"); - } + } - [Test] - public void TestPipeTable2() - { - // 01 2 3456 789A BCD - Check("0\n\na|b\n-|-\n0|1\n", @" + [Test] + public void TestPipeTable2() + { + // 01 2 3456 789A BCD + Check("0\n\na|b\n-|-\n0|1\n", @" paragraph ( 0, 0) 0-0 literal ( 0, 0) 0-0 table ( 2, 0) 3-13 @@ -724,60 +721,60 @@ public void TestPipeTable2() paragraph ( 4, 2) 13-13 literal ( 4, 2) 13-13 ", "pipetables"); - } + } - [Test] - public void TestIndentedCode() - { - // 01 2 345678 9ABCDE - Check("0\n\n 0\n 1\n", @" + [Test] + public void TestIndentedCode() + { + // 01 2 345678 9ABCDE + Check("0\n\n 0\n 1\n", @" paragraph ( 0, 0) 0-0 literal ( 0, 0) 0-0 code ( 2, 0) 3-13 "); - } + } - [Test] - public void TestIndentedCodeAfterList() - { - // 0 1 2 3 4 5 - // 012345678901234567 8 901234567890123456 789012345678901234 56789 - Check("1) Some list item\n\n some code\n more code\n", @" + [Test] + public void TestIndentedCodeAfterList() + { + // 0 1 2 3 4 5 + // 012345678901234567 8 901234567890123456 789012345678901234 56789 + Check("1) Some list item\n\n some code\n more code\n", @" list ( 0, 0) 0-53 listitem ( 0, 0) 0-53 paragraph ( 0, 3) 3-16 literal ( 0, 3) 3-16 code ( 2, 0) 19-53 "); - } + } - [Test] - public void TestIndentedCodeWithTabs() - { - // 01 2 3 45 6 78 - Check("0\n\n\t0\n\t1\n", @" + [Test] + public void TestIndentedCodeWithTabs() + { + // 01 2 3 45 6 78 + Check("0\n\n\t0\n\t1\n", @" paragraph ( 0, 0) 0-0 literal ( 0, 0) 0-0 code ( 2, 0) 3-7 "); - } + } - [Test] - public void TestIndentedCodeWithMixedTabs() - { - // 01 2 34 56 78 9 - Check("0\n\n \t0\n \t1\n", @" + [Test] + public void TestIndentedCodeWithMixedTabs() + { + // 01 2 34 56 78 9 + Check("0\n\n \t0\n \t1\n", @" paragraph ( 0, 0) 0-0 literal ( 0, 0) 0-0 code ( 2, 0) 3-9 "); - } + } - [Test] - public void TestTabsInList() - { - // 012 34 567 89 - Check("- \t0\n- \t1\n", @" + [Test] + public void TestTabsInList() + { + // 012 34 567 89 + Check("- \t0\n- \t1\n", @" list ( 0, 0) 0-8 listitem ( 0, 0) 0-3 paragraph ( 0, 4) 3-3 @@ -786,15 +783,15 @@ public void TestTabsInList() paragraph ( 1, 4) 8-8 literal ( 1, 4) 8-8 "); - } + } - [Test] - public void TestDocument() - { - // L0 L0 L1L2 L3 L4 L5L6 L7L8 - // 0 10 20 30 40 50 60 70 80 90 - // 012345678901234567890 1 2345678901 2345678901 2345678901 2 345678901234567890123 4 5678901234567890123 - Check("# This is a document\n\n1) item 1\n2) item 2\n3) item 4\n\nWith an **emphasis**\n\n> and a blockquote\n", @" + [Test] + public void TestDocument() + { + // L0 L0 L1L2 L3 L4 L5L6 L7L8 + // 0 10 20 30 40 50 60 70 80 90 + // 012345678901234567890 1 2345678901 2345678901 2345678901 2 345678901234567890123 4 5678901234567890123 + Check("# This is a document\n\n1) item 1\n2) item 2\n3) item 4\n\nWith an **emphasis**\n\n> and a blockquote\n", @" heading ( 0, 0) 0-19 literal ( 0, 2) 2-19 list ( 2, 0) 22-51 @@ -815,55 +812,54 @@ public void TestDocument() paragraph ( 8, 2) 77-92 literal ( 8, 2) 77-92 "); - } + } - private static void Check(string text, string expectedResult, string extensions = null) + private static void Check(string text, string expectedResult, string extensions = null) + { + var pipelineBuilder = new MarkdownPipelineBuilder().UsePreciseSourceLocation(); + if (extensions != null) { - var pipelineBuilder = new MarkdownPipelineBuilder().UsePreciseSourceLocation(); - if (extensions != null) - { - pipelineBuilder.Configure(extensions); - } - var pipeline = pipelineBuilder.Build(); - - var document = Markdown.Parse(text, pipeline); - - var build = new StringBuilder(); - foreach (var val in document.Descendants()) - { - var name = GetTypeName(val.GetType()); - build.Append($"{name,-12} ({val.Line,2},{val.Column,2}) {val.Span.Start,2}-{val.Span.End}\n"); - var attributes = val.TryGetAttributes(); - if (attributes != null) - { - build.Append($"{"attributes",-12} ({attributes.Line,2},{attributes.Column,2}) {attributes.Span.Start,2}-{attributes.Span.End}\n"); - } - } - var result = build.ToString().Trim(); + pipelineBuilder.Configure(extensions); + } + var pipeline = pipelineBuilder.Build(); - expectedResult = expectedResult.Trim(); - expectedResult = expectedResult.Replace("\r\n", "\n").Replace("\r", "\n"); + var document = Markdown.Parse(text, pipeline); - if (expectedResult != result) + var build = new StringBuilder(); + foreach (var val in document.Descendants()) + { + var name = GetTypeName(val.GetType()); + build.Append($"{name,-12} ({val.Line,2},{val.Column,2}) {val.Span.Start,2}-{val.Span.End}\n"); + var attributes = val.TryGetAttributes(); + if (attributes != null) { - Console.WriteLine("```````````````````Source"); - Console.WriteLine(TestParser.DisplaySpaceAndTabs(text)); - Console.WriteLine("```````````````````Result"); - Console.WriteLine(result); - Console.WriteLine("```````````````````Expected"); - Console.WriteLine(expectedResult); - Console.WriteLine("```````````````````"); - Console.WriteLine(); + build.Append($"{"attributes",-12} ({attributes.Line,2},{attributes.Column,2}) {attributes.Span.Start,2}-{attributes.Span.End}\n"); } - - TextAssert.AreEqual(expectedResult, result); } + var result = build.ToString().Trim(); - private static string GetTypeName(Type type) + expectedResult = expectedResult.Trim(); + expectedResult = expectedResult.Replace("\r\n", "\n").Replace("\r", "\n"); + + if (expectedResult != result) { - return type.Name.ToLowerInvariant() - .Replace("block", string.Empty) - .Replace("inline", string.Empty); + Console.WriteLine("```````````````````Source"); + Console.WriteLine(TestParser.DisplaySpaceAndTabs(text)); + Console.WriteLine("```````````````````Result"); + Console.WriteLine(result); + Console.WriteLine("```````````````````Expected"); + Console.WriteLine(expectedResult); + Console.WriteLine("```````````````````"); + Console.WriteLine(); } + + TextAssert.AreEqual(expectedResult, result); + } + + private static string GetTypeName(Type type) + { + return type.Name.ToLowerInvariant() + .Replace("block", string.Empty) + .Replace("inline", string.Empty); } } \ No newline at end of file diff --git a/src/Markdig.Tests/TestStringSliceList.cs b/src/Markdig.Tests/TestStringSliceList.cs index 0279d58fa..8849e7d63 100644 --- a/src/Markdig.Tests/TestStringSliceList.cs +++ b/src/Markdig.Tests/TestStringSliceList.cs @@ -1,185 +1,183 @@ -using NUnit.Framework; using System.Text; + using Markdig.Helpers; -using System; -namespace Markdig.Tests +namespace Markdig.Tests; + +[TestFixture] +public class TestStringSliceList { - [TestFixture] - public class TestStringSliceList - { - // TODO: Add tests for StringSlice - // TODO: Add more tests for StringLineGroup + // TODO: Add tests for StringSlice + // TODO: Add more tests for StringLineGroup - [Test] - public void TestStringLineGroupSimple() + [Test] + public void TestStringLineGroupSimple() + { + var text = new StringLineGroup(4) { - var text = new StringLineGroup(4) - { - new StringSlice("ABC", NewLine.LineFeed), - new StringSlice("E", NewLine.LineFeed), - new StringSlice("F") - }; + new StringSlice("ABC", NewLine.LineFeed), + new StringSlice("E", NewLine.LineFeed), + new StringSlice("F") + }; - var iterator = text.ToCharIterator(); - Assert.AreEqual("ABC\nE\nF".Length, iterator.End - iterator.Start + 1); + var iterator = text.ToCharIterator(); + Assert.AreEqual("ABC\nE\nF".Length, iterator.End - iterator.Start + 1); - var chars = ToString(text.ToCharIterator()); - TextAssert.AreEqual("ABC\nE\nF", chars.ToString()); + var chars = ToString(text.ToCharIterator()); + TextAssert.AreEqual("ABC\nE\nF", chars.ToString()); - TextAssert.AreEqual("ABC\nE\nF", text.ToString()); - } + TextAssert.AreEqual("ABC\nE\nF", text.ToString()); + } - [Test] - public void TestStringLineGroupWithSlices() + [Test] + public void TestStringLineGroupWithSlices() + { + var text = new StringLineGroup(4) { - var text = new StringLineGroup(4) - { - new StringSlice("XABC", NewLine.LineFeed) { Start = 1}, - new StringSlice("YYE", NewLine.LineFeed) { Start = 2}, - new StringSlice("ZZZF") { Start = 3 } - }; - - var chars = ToString(text.ToCharIterator()); - TextAssert.AreEqual("ABC\nE\nF", chars.ToString()); - } + new StringSlice("XABC", NewLine.LineFeed) { Start = 1}, + new StringSlice("YYE", NewLine.LineFeed) { Start = 2}, + new StringSlice("ZZZF") { Start = 3 } + }; + + var chars = ToString(text.ToCharIterator()); + TextAssert.AreEqual("ABC\nE\nF", chars.ToString()); + } - private static string ToString(StringLineGroup.Iterator text) + private static string ToString(StringLineGroup.Iterator text) + { + var chars = new StringBuilder(); + while (text.CurrentChar != '\0') { - var chars = new StringBuilder(); - while (text.CurrentChar != '\0') - { - chars.Append(text.CurrentChar); - text.NextChar(); - } - return chars.ToString(); + chars.Append(text.CurrentChar); + text.NextChar(); } + return chars.ToString(); + } - [Test] - public void TestStringLineGroupSaveAndRestore() + [Test] + public void TestStringLineGroupSaveAndRestore() + { + var text = new StringLineGroup(4) { - var text = new StringLineGroup(4) - { - new StringSlice("ABCD", NewLine.LineFeed), - new StringSlice("EF"), - }.ToCharIterator(); + new StringSlice("ABCD", NewLine.LineFeed), + new StringSlice("EF"), + }.ToCharIterator(); - Assert.AreEqual('A', text.CurrentChar); - Assert.AreEqual(0, text.SliceIndex); + Assert.AreEqual('A', text.CurrentChar); + Assert.AreEqual(0, text.SliceIndex); - text.NextChar(); // B + text.NextChar(); // B - text.NextChar(); // C - text.NextChar(); // D - text.NextChar(); // \n - text.NextChar(); - Assert.AreEqual('E', text.CurrentChar); - Assert.AreEqual(1, text.SliceIndex); - } + text.NextChar(); // C + text.NextChar(); // D + text.NextChar(); // \n + text.NextChar(); + Assert.AreEqual('E', text.CurrentChar); + Assert.AreEqual(1, text.SliceIndex); + } - [Test] - public void TestSkipWhitespaces() - { - var text = new StringLineGroup(" ABC").ToCharIterator(); - Assert.False(text.TrimStart()); - Assert.AreEqual('A', text.CurrentChar); + [Test] + public void TestSkipWhitespaces() + { + var text = new StringLineGroup(" ABC").ToCharIterator(); + Assert.False(text.TrimStart()); + Assert.AreEqual('A', text.CurrentChar); - text = new StringLineGroup(" ").ToCharIterator(); - Assert.True(text.TrimStart()); - Assert.AreEqual('\0', text.CurrentChar); + text = new StringLineGroup(" ").ToCharIterator(); + Assert.True(text.TrimStart()); + Assert.AreEqual('\0', text.CurrentChar); - var slice = new StringSlice(" ABC"); - Assert.False(slice.TrimStart()); + var slice = new StringSlice(" ABC"); + Assert.False(slice.TrimStart()); - slice = new StringSlice(" "); - Assert.True(slice.TrimStart()); - } + slice = new StringSlice(" "); + Assert.True(slice.TrimStart()); + } - [Test] - public void TestStringLineGroupWithModifiedStart() - { - var line1 = new StringSlice(" ABC", NewLine.LineFeed); - line1.NextChar(); - line1.NextChar(); + [Test] + public void TestStringLineGroupWithModifiedStart() + { + var line1 = new StringSlice(" ABC", NewLine.LineFeed); + line1.NextChar(); + line1.NextChar(); - var line2 = new StringSlice(" DEF "); - line2.Trim(); + var line2 = new StringSlice(" DEF "); + line2.Trim(); - var text = new StringLineGroup(4) {line1, line2}; + var text = new StringLineGroup(4) {line1, line2}; - var result = ToString(text.ToCharIterator()); - TextAssert.AreEqual("ABC\nDEF", result); - } + var result = ToString(text.ToCharIterator()); + TextAssert.AreEqual("ABC\nDEF", result); + } - [Test] - public void TestStringLineGroupWithTrim() - { - var line1 = new StringSlice(" ABC ", NewLine.LineFeed); - line1.NextChar(); - line1.NextChar(); + [Test] + public void TestStringLineGroupWithTrim() + { + var line1 = new StringSlice(" ABC ", NewLine.LineFeed); + line1.NextChar(); + line1.NextChar(); - var line2 = new StringSlice(" DEF "); + var line2 = new StringSlice(" DEF "); - var text = new StringLineGroup(4) { line1, line2}.ToCharIterator(); - text.TrimStart(); + var text = new StringLineGroup(4) { line1, line2}.ToCharIterator(); + text.TrimStart(); - var result = ToString(text); - TextAssert.AreEqual("ABC \n DEF ", result); - } + var result = ToString(text); + TextAssert.AreEqual("ABC \n DEF ", result); + } - [Test] - public void TestStringLineGroupIteratorPeekChar() + [Test] + public void TestStringLineGroupIteratorPeekChar() + { + var iterator = new StringLineGroup(4) { - var iterator = new StringLineGroup(4) - { - new StringSlice("ABC", NewLine.LineFeed), - new StringSlice("E", NewLine.LineFeed), - new StringSlice("F") - }.ToCharIterator(); - - Assert.AreEqual('A', iterator.CurrentChar); - Assert.AreEqual('A', iterator.PeekChar(0)); - Assert.AreEqual('B', iterator.PeekChar()); - Assert.AreEqual('B', iterator.PeekChar(1)); - Assert.AreEqual('C', iterator.PeekChar(2)); - Assert.AreEqual('\n', iterator.PeekChar(3)); - Assert.AreEqual('E', iterator.PeekChar(4)); - Assert.AreEqual('\n', iterator.PeekChar(5)); - Assert.AreEqual('F', iterator.PeekChar(6)); - Assert.AreEqual('\0', iterator.PeekChar(7)); // There is no \n appended to the last line - Assert.AreEqual('\0', iterator.PeekChar(8)); - Assert.AreEqual('\0', iterator.PeekChar(100)); - - Assert.Throws(() => iterator.PeekChar(-1)); - } + new StringSlice("ABC", NewLine.LineFeed), + new StringSlice("E", NewLine.LineFeed), + new StringSlice("F") + }.ToCharIterator(); + + Assert.AreEqual('A', iterator.CurrentChar); + Assert.AreEqual('A', iterator.PeekChar(0)); + Assert.AreEqual('B', iterator.PeekChar()); + Assert.AreEqual('B', iterator.PeekChar(1)); + Assert.AreEqual('C', iterator.PeekChar(2)); + Assert.AreEqual('\n', iterator.PeekChar(3)); + Assert.AreEqual('E', iterator.PeekChar(4)); + Assert.AreEqual('\n', iterator.PeekChar(5)); + Assert.AreEqual('F', iterator.PeekChar(6)); + Assert.AreEqual('\0', iterator.PeekChar(7)); // There is no \n appended to the last line + Assert.AreEqual('\0', iterator.PeekChar(8)); + Assert.AreEqual('\0', iterator.PeekChar(100)); + + Assert.Throws(() => iterator.PeekChar(-1)); + } + + [Test] + public void TestIteratorSkipChar() + { + var lineGroup = new StringLineGroup(4) + { + new StringSlice("ABC", NewLine.LineFeed), + new StringSlice("E", NewLine.LineFeed) + }; + + Test(lineGroup.ToCharIterator()); + + Test(new StringSlice("ABC\nE\n")); + + Test(new StringSlice("Foo\nABC\nE\n", 4, 9)); - [Test] - public void TestIteratorSkipChar() + static void Test(T iterator) where T : ICharIterator { - var lineGroup = new StringLineGroup(4) - { - new StringSlice("ABC", NewLine.LineFeed), - new StringSlice("E", NewLine.LineFeed) - }; - - Test(lineGroup.ToCharIterator()); - - Test(new StringSlice("ABC\nE\n")); - - Test(new StringSlice("Foo\nABC\nE\n", 4, 9)); - - static void Test(T iterator) where T : ICharIterator - { - Assert.AreEqual('A', iterator.CurrentChar); iterator.SkipChar(); - Assert.AreEqual('B', iterator.CurrentChar); iterator.SkipChar(); - Assert.AreEqual('C', iterator.CurrentChar); iterator.SkipChar(); - Assert.AreEqual('\n', iterator.CurrentChar); iterator.SkipChar(); - Assert.AreEqual('E', iterator.CurrentChar); iterator.SkipChar(); - Assert.AreEqual('\n', iterator.CurrentChar); iterator.SkipChar(); - Assert.AreEqual('\0', iterator.CurrentChar); iterator.SkipChar(); - Assert.AreEqual('\0', iterator.CurrentChar); iterator.SkipChar(); - } + Assert.AreEqual('A', iterator.CurrentChar); iterator.SkipChar(); + Assert.AreEqual('B', iterator.CurrentChar); iterator.SkipChar(); + Assert.AreEqual('C', iterator.CurrentChar); iterator.SkipChar(); + Assert.AreEqual('\n', iterator.CurrentChar); iterator.SkipChar(); + Assert.AreEqual('E', iterator.CurrentChar); iterator.SkipChar(); + Assert.AreEqual('\n', iterator.CurrentChar); iterator.SkipChar(); + Assert.AreEqual('\0', iterator.CurrentChar); iterator.SkipChar(); + Assert.AreEqual('\0', iterator.CurrentChar); iterator.SkipChar(); } } } \ No newline at end of file diff --git a/src/Markdig.Tests/TestTransformedStringCache.cs b/src/Markdig.Tests/TestTransformedStringCache.cs index ec9a7dc22..0a21c0b66 100644 --- a/src/Markdig.Tests/TestTransformedStringCache.cs +++ b/src/Markdig.Tests/TestTransformedStringCache.cs @@ -1,102 +1,98 @@ -using System; -using System.Linq; using Markdig.Helpers; -using NUnit.Framework; -namespace Markdig.Tests +namespace Markdig.Tests; + +public class TestTransformedStringCache { - public class TestTransformedStringCache + [Test] + public void GetRunsTransformationCallback() { - [Test] - public void GetRunsTransformationCallback() - { - var cache = new TransformedStringCache(static s => "callback-" + s); + 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")); - } + 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); + [Test] + public void CachesTransformedInstance() + { + var cache = new TransformedStringCache(static s => "callback-" + s); - string transformedBar = cache.Get("bar"); - Assert.AreSame(transformedBar, cache.Get("bar")); + string transformedBar = cache.Get("bar"); + Assert.AreSame(transformedBar, cache.Get("bar")); - string transformedFoo = cache.Get("foo".AsSpan()); - Assert.AreSame(transformedFoo, cache.Get("foo")); + string transformedFoo = cache.Get("foo".AsSpan()); + Assert.AreSame(transformedFoo, cache.Get("foo")); - Assert.AreSame(cache.Get("baz"), cache.Get("baz".AsSpan())); + 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())); - } + 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)); + [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.Empty); + string cached = cache.Get(""); + string cached2 = cache.Get(""); + string cached3 = cache.Get(ReadOnlySpan.Empty); - Assert.AreEqual("aaaa", cached); - Assert.AreEqual(cached, cached2); - Assert.AreEqual(cached, cached3); + Assert.AreEqual("aaaa", cached); + Assert.AreEqual(cached, cached2); + Assert.AreEqual(cached, cached3); - Assert.AreNotSame(cached, cached2); - Assert.AreNotSame(cached, cached3); - Assert.AreNotSame(cached2, 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); + [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 input = new string('a', length); - string cached = cache.Get(input); - string cached2 = cache.Get(input); + string cached = cache.Get(input); + string cached2 = cache.Get(input); - Assert.AreEqual("callback-" + input, cached); - Assert.AreEqual(cached, cached2); + Assert.AreEqual("callback-" + input, cached); + Assert.AreEqual(cached, cached2); - if (shouldBeCached) - { - Assert.AreSame(cached, cached2); - } - else - { - Assert.AreNotSame(cached, cached2); - } + if (shouldBeCached) + { + Assert.AreSame(cached, cached2); } - - [Test] - public void CachesAtMostNEntriesPerCharacter() + else { - var cache = new TransformedStringCache(static s => "callback-" + s); - - int limit = TransformedStringCache.MaxEntriesPerCharacter; + Assert.AreNotSame(cached, cached2); + } + } - string[] a = Enumerable.Range(1, limit + 1).Select(i => $"a{i}").ToArray(); - string[] cachedAs = a.Select(a => cache.Get(a)).ToArray(); + [Test] + public void CachesAtMostNEntriesPerCharacter() + { + var cache = new TransformedStringCache(static s => "callback-" + s); - for (int i = 0; i < limit; i++) - { - Assert.AreSame(cachedAs[i], cache.Get(a[i])); - } + int limit = TransformedStringCache.MaxEntriesPerCharacter; - Assert.AreNotSame(cachedAs[limit], cache.Get(a[limit])); + string[] a = Enumerable.Range(1, limit + 1).Select(i => $"a{i}").ToArray(); + string[] cachedAs = a.Select(a => cache.Get(a)).ToArray(); - Assert.AreSame(cache.Get("b1"), cache.Get("b1")); + 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")); } } diff --git a/src/Markdig.Tests/TestYamlFrontMatterExtension.cs b/src/Markdig.Tests/TestYamlFrontMatterExtension.cs index b5bec9a90..c62e4db6c 100644 --- a/src/Markdig.Tests/TestYamlFrontMatterExtension.cs +++ b/src/Markdig.Tests/TestYamlFrontMatterExtension.cs @@ -1,78 +1,73 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Markdig.Extensions.Yaml; using Markdig.Renderers; using Markdig.Syntax; -using NUnit.Framework; -namespace Markdig.Tests +namespace Markdig.Tests; + +public class TestYamlFrontMatterExtension { - public class TestYamlFrontMatterExtension + [TestCaseSource(nameof(TestCases))] + public void ProperYamlFrontMatterRenderersAdded(IMarkdownObjectRenderer[] objectRenderers, bool hasYamlFrontMatterHtmlRenderer, bool hasYamlFrontMatterRoundtripRenderer) + { + var builder = new MarkdownPipelineBuilder(); + builder.Extensions.Add(new YamlFrontMatterExtension()); + var markdownRenderer = new DummyRenderer(); + markdownRenderer.ObjectRenderers.AddRange(objectRenderers); + builder.Build().Setup(markdownRenderer); + Assert.That(markdownRenderer.ObjectRenderers.Contains(), Is.EqualTo(hasYamlFrontMatterHtmlRenderer)); + Assert.That(markdownRenderer.ObjectRenderers.Contains(), Is.EqualTo(hasYamlFrontMatterRoundtripRenderer)); + } + + private static IEnumerable TestCases() { - [TestCaseSource(nameof(TestCases))] - public void ProperYamlFrontMatterRenderersAdded(IMarkdownObjectRenderer[] objectRenderers, bool hasYamlFrontMatterHtmlRenderer, bool hasYamlFrontMatterRoundtripRenderer) + yield return new TestCaseData(new IMarkdownObjectRenderer[] { - var builder = new MarkdownPipelineBuilder(); - builder.Extensions.Add(new YamlFrontMatterExtension()); - var markdownRenderer = new DummyRenderer(); - markdownRenderer.ObjectRenderers.AddRange(objectRenderers); - builder.Build().Setup(markdownRenderer); - Assert.That(markdownRenderer.ObjectRenderers.Contains(), Is.EqualTo(hasYamlFrontMatterHtmlRenderer)); - Assert.That(markdownRenderer.ObjectRenderers.Contains(), Is.EqualTo(hasYamlFrontMatterRoundtripRenderer)); - } + }, false, false) {TestName = "No ObjectRenderers"}; - private static IEnumerable TestCases() + yield return new TestCaseData(new IMarkdownObjectRenderer[] { - yield return new TestCaseData(new IMarkdownObjectRenderer[] - { - }, false, false) {TestName = "No ObjectRenderers"}; + new Markdig.Renderers.Html.CodeBlockRenderer() + }, true, false) {TestName = "Html CodeBlock"}; - yield return new TestCaseData(new IMarkdownObjectRenderer[] - { - new Markdig.Renderers.Html.CodeBlockRenderer() - }, true, false) {TestName = "Html CodeBlock"}; + yield return new TestCaseData(new IMarkdownObjectRenderer[] + { + new Markdig.Renderers.Roundtrip.CodeBlockRenderer() + }, false, true) {TestName = "Roundtrip CodeBlock"}; - yield return new TestCaseData(new IMarkdownObjectRenderer[] - { - new Markdig.Renderers.Roundtrip.CodeBlockRenderer() - }, false, true) {TestName = "Roundtrip CodeBlock"}; + yield return new TestCaseData(new IMarkdownObjectRenderer[] + { + new Markdig.Renderers.Html.CodeBlockRenderer(), + new Markdig.Renderers.Roundtrip.CodeBlockRenderer() + }, true, true) {TestName = "Html/Roundtrip CodeBlock"}; - yield return new TestCaseData(new IMarkdownObjectRenderer[] - { - new Markdig.Renderers.Html.CodeBlockRenderer(), - new Markdig.Renderers.Roundtrip.CodeBlockRenderer() - }, true, true) {TestName = "Html/Roundtrip CodeBlock"}; + yield return new TestCaseData(new IMarkdownObjectRenderer[] + { + new Markdig.Renderers.Html.CodeBlockRenderer(), + new Markdig.Renderers.Roundtrip.CodeBlockRenderer(), + new YamlFrontMatterHtmlRenderer() + }, true, true) {TestName = "Html/Roundtrip CodeBlock, Yaml Html"}; - yield return new TestCaseData(new IMarkdownObjectRenderer[] - { - new Markdig.Renderers.Html.CodeBlockRenderer(), - new Markdig.Renderers.Roundtrip.CodeBlockRenderer(), - new YamlFrontMatterHtmlRenderer() - }, true, true) {TestName = "Html/Roundtrip CodeBlock, Yaml Html"}; + yield return new TestCaseData(new IMarkdownObjectRenderer[] + { + new Markdig.Renderers.Html.CodeBlockRenderer(), + new Markdig.Renderers.Roundtrip.CodeBlockRenderer(), + new YamlFrontMatterRoundtripRenderer() + }, true, true) { TestName = "Html/Roundtrip CodeBlock, Yaml Roundtrip" }; + } - yield return new TestCaseData(new IMarkdownObjectRenderer[] - { - new Markdig.Renderers.Html.CodeBlockRenderer(), - new Markdig.Renderers.Roundtrip.CodeBlockRenderer(), - new YamlFrontMatterRoundtripRenderer() - }, true, true) { TestName = "Html/Roundtrip CodeBlock, Yaml Roundtrip" }; + private class DummyRenderer : IMarkdownRenderer + { + public DummyRenderer() + { + ObjectRenderers = new ObjectRendererCollection(); } - private class DummyRenderer : IMarkdownRenderer + public event Action ObjectWriteBefore; + public event Action ObjectWriteAfter; + public ObjectRendererCollection ObjectRenderers { get; } + public object Render(MarkdownObject markdownObject) { - public DummyRenderer() - { - ObjectRenderers = new ObjectRendererCollection(); - } - - public event Action ObjectWriteBefore; - public event Action ObjectWriteAfter; - public ObjectRendererCollection ObjectRenderers { get; } - public object Render(MarkdownObject markdownObject) - { - return null; - } + return null; } } } diff --git a/src/Markdig.Tests/TextAssert.cs b/src/Markdig.Tests/TextAssert.cs index 1521736d8..275892626 100644 --- a/src/Markdig.Tests/TextAssert.cs +++ b/src/Markdig.Tests/TextAssert.cs @@ -1,119 +1,115 @@ // 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.Globalization; -using System.IO; -using NUnit.Framework; -namespace Markdig.Tests +namespace Markdig.Tests; + +/// +/// Pretty text assert from https://gist.github.com/Haacked/1610603 +/// Modified version to only print +-10 characters around the first diff +/// +public static class TextAssert { - /// - /// Pretty text assert from https://gist.github.com/Haacked/1610603 - /// Modified version to only print +-10 characters around the first diff - /// - public static class TextAssert + public enum DiffStyle { - public enum DiffStyle - { - Full, - Minimal - } + Full, + Minimal + } - public static void AreEqual(string expectedValue, string actualValue) - { - AreEqual(expectedValue, actualValue, DiffStyle.Full, Console.Out); - } + public static void AreEqual(string expectedValue, string actualValue) + { + AreEqual(expectedValue, actualValue, DiffStyle.Full, Console.Out); + } - public static void AreEqual(string expectedValue, string actualValue, DiffStyle diffStyle) + public static void AreEqual(string expectedValue, string actualValue, DiffStyle diffStyle) + { + AreEqual(expectedValue, actualValue, diffStyle, Console.Out); + } + + public static void AreEqual(string expectedValue, string actualValue, DiffStyle diffStyle, TextWriter output) + { + if (actualValue == null || expectedValue == null) { - AreEqual(expectedValue, actualValue, diffStyle, Console.Out); + Assert.AreEqual(expectedValue, actualValue); + return; } - public static void AreEqual(string expectedValue, string actualValue, DiffStyle diffStyle, TextWriter output) + if (actualValue.Equals(expectedValue, StringComparison.Ordinal)) { - if (actualValue == null || expectedValue == null) - { - Assert.AreEqual(expectedValue, actualValue); - return; - } - - if (actualValue.Equals(expectedValue, StringComparison.Ordinal)) - { - return; - } + return; + } - Console.WriteLine(); - output.WriteLine("Index Expected Actual"); - output.WriteLine("----------------------------"); - int maxLen = Math.Max(actualValue.Length, expectedValue.Length); - int minLen = Math.Min(actualValue.Length, expectedValue.Length); + Console.WriteLine(); + output.WriteLine("Index Expected Actual"); + output.WriteLine("----------------------------"); + int maxLen = Math.Max(actualValue.Length, expectedValue.Length); + int minLen = Math.Min(actualValue.Length, expectedValue.Length); - if (diffStyle != DiffStyle.Minimal) + if (diffStyle != DiffStyle.Minimal) + { + int startDifferAt = 0; + for (int i = 0; i < maxLen; i++) { - int startDifferAt = 0; - for (int i = 0; i < maxLen; i++) + if (i >= minLen || actualValue[i] != expectedValue[i]) { - if (i >= minLen || actualValue[i] != expectedValue[i]) - { - startDifferAt = i; - break; - } + startDifferAt = i; + break; } + } - var endDifferAt = Math.Min(startDifferAt + 10, maxLen); - startDifferAt = Math.Max(startDifferAt - 10, 0); + var endDifferAt = Math.Min(startDifferAt + 10, maxLen); + startDifferAt = Math.Max(startDifferAt - 10, 0); - bool isFirstDiff = true; - for (int i = startDifferAt; i < endDifferAt; i++) + bool isFirstDiff = true; + for (int i = startDifferAt; i < endDifferAt; i++) + { + if (i >= minLen || actualValue[i] != expectedValue[i]) { - if (i >= minLen || actualValue[i] != expectedValue[i]) - { - output.WriteLine("{0,-3} {1,-3} {2,-4} {3,-3} {4,-4} {5,-3}", - i < minLen && actualValue[i] == expectedValue[i] ? " " : isFirstDiff ? ">>>": "***", - // put a mark beside a differing row - i, // the index - i < expectedValue.Length ? ((int) expectedValue[i]).ToString() : "", - // character decimal value - i < expectedValue.Length ? expectedValue[i].ToSafeString() : "", // character safe string - i < actualValue.Length ? ((int) actualValue[i]).ToString() : "", // character decimal value - i < actualValue.Length ? actualValue[i].ToSafeString() : "" // character safe string - ); + output.WriteLine("{0,-3} {1,-3} {2,-4} {3,-3} {4,-4} {5,-3}", + i < minLen && actualValue[i] == expectedValue[i] ? " " : isFirstDiff ? ">>>": "***", + // put a mark beside a differing row + i, // the index + i < expectedValue.Length ? ((int) expectedValue[i]).ToString() : "", + // character decimal value + i < expectedValue.Length ? expectedValue[i].ToSafeString() : "", // character safe string + i < actualValue.Length ? ((int) actualValue[i]).ToString() : "", // character decimal value + i < actualValue.Length ? actualValue[i].ToSafeString() : "" // character safe string + ); - isFirstDiff = false; - } + isFirstDiff = false; } - //output.WriteLine(); } - - Assert.AreEqual(expectedValue, actualValue); + //output.WriteLine(); } - private static string ToSafeString(this char c) + Assert.AreEqual(expectedValue, actualValue); + } + + private static string ToSafeString(this char c) + { + if (char.IsControl(c) || char.IsWhiteSpace(c)) { - if (char.IsControl(c) || char.IsWhiteSpace(c)) + switch (c) { - switch (c) - { - case '\b': - return @"\b"; - case '\r': - return @"\r"; - case '\n': - return @"\n"; - case '\t': - return @"\t"; - case '\a': - return @"\a"; - case '\v': - return @"\v"; - case '\f': - return @"\f"; - default: - return $"\\u{(int) c:X};"; - } + case '\b': + return @"\b"; + case '\r': + return @"\r"; + case '\n': + return @"\n"; + case '\t': + return @"\t"; + case '\a': + return @"\a"; + case '\v': + return @"\v"; + case '\f': + return @"\f"; + default: + return $"\\u{(int) c:X};"; } - return c.ToString(CultureInfo.InvariantCulture); } + return c.ToString(CultureInfo.InvariantCulture); } } \ No newline at end of file diff --git a/src/Markdig.WebApp/ApiController.cs b/src/Markdig.WebApp/ApiController.cs index 4bbbb09ca..2018c9366 100644 --- a/src/Markdig.WebApp/ApiController.cs +++ b/src/Markdig.WebApp/ApiController.cs @@ -1,55 +1,54 @@ -using System; using System.Text; + using Microsoft.AspNetCore.Mvc; -namespace Markdig.WebApp +namespace Markdig.WebApp; + +public class ApiController : Controller { - public class ApiController : Controller + [HttpGet()] + [Route("")] + public string Empty() { - [HttpGet()] - [Route("")] - public string Empty() - { - return string.Empty; - } + return string.Empty; + } - // GET api/to_html?text=xxx&extension=advanced - [Route("api/to_html")] - [HttpGet()] - public object Get([FromQuery] string text, [FromQuery] string extension) + // GET api/to_html?text=xxx&extension=advanced + [Route("api/to_html")] + [HttpGet()] + public object Get([FromQuery] string text, [FromQuery] string extension) + { + try { - try + if (text == null) { - if (text == null) - { - text = string.Empty; - } - - if (text.Length > 1000) - { - text = text.Substring(0, 1000); - } - - var pipeline = new MarkdownPipelineBuilder().Configure(extension).Build(); - var result = Markdown.ToHtml(text, pipeline); - - return new {name = "markdig", html = result, version = Markdown.Version}; + text = string.Empty; } - catch (Exception ex) + + if (text.Length > 1000) { - return new { name = "markdig", html = "exception: " + GetPrettyMessageFromException(ex), version = Markdown.Version }; + text = text.Substring(0, 1000); } + + var pipeline = new MarkdownPipelineBuilder().Configure(extension).Build(); + var result = Markdown.ToHtml(text, pipeline); + + return new {name = "markdig", html = result, version = Markdown.Version}; + } + catch (Exception ex) + { + return new { name = "markdig", html = "exception: " + GetPrettyMessageFromException(ex), version = Markdown.Version }; } + } - private static string GetPrettyMessageFromException(Exception exception) + private static string GetPrettyMessageFromException(Exception exception) + { + var builder = new StringBuilder(); + while (exception != null) { - var builder = new StringBuilder(); - while (exception != null) - { - builder.Append(exception.Message); - exception = exception.InnerException; - } - return builder.ToString(); + builder.Append(exception.Message); + exception = exception.InnerException; } + return builder.ToString(); } } diff --git a/src/Markdig.WebApp/Markdig.WebApp.csproj b/src/Markdig.WebApp/Markdig.WebApp.csproj index 7ef583ee8..d8cb2f0e7 100644 --- a/src/Markdig.WebApp/Markdig.WebApp.csproj +++ b/src/Markdig.WebApp/Markdig.WebApp.csproj @@ -4,6 +4,7 @@ net6.0 true Markdig.WebApp + enable Exe Markdig.WebApp /subscriptions/b6745039-70e7-4641-994b-5457cb220e2a/resourcegroups/Default-ApplicationInsights-EastUS/providers/microsoft.insights/components/Markdig.WebApp diff --git a/src/Markdig.WebApp/Program.cs b/src/Markdig.WebApp/Program.cs index c17a2d1d9..80487b845 100644 --- a/src/Markdig.WebApp/Program.cs +++ b/src/Markdig.WebApp/Program.cs @@ -1,22 +1,16 @@ -using System.IO; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Hosting; +namespace Markdig.WebApp; -namespace Markdig.WebApp +public class Program { - public class Program + public static void Main(string[] args) { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); + CreateHostBuilder(args).Build().Run(); } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); } diff --git a/src/Markdig.WebApp/Startup.cs b/src/Markdig.WebApp/Startup.cs index 28701ec11..ce84a9772 100644 --- a/src/Markdig.WebApp/Startup.cs +++ b/src/Markdig.WebApp/Startup.cs @@ -1,60 +1,52 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace Markdig.WebApp +namespace Markdig.WebApp; + +public class Startup { - public class Startup + public Startup(IWebHostEnvironment env) { - public Startup(IWebHostEnvironment env) + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true); + + if (env.IsEnvironment("Development")) { - var builder = new ConfigurationBuilder() - .SetBasePath(env.ContentRootPath) - .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true); - - if (env.IsEnvironment("Development")) - { - // This will push telemetry data through Application Insights pipeline faster, allowing you to view results immediately. - builder.AddApplicationInsightsSettings(developerMode: true); - } - - builder.AddEnvironmentVariables(); - Configuration = builder.Build(); + // This will push telemetry data through Application Insights pipeline faster, allowing you to view results immediately. + builder.AddApplicationInsightsSettings(developerMode: true); } - public IConfigurationRoot Configuration { get; } + builder.AddEnvironmentVariables(); + Configuration = builder.Build(); + } + + public IConfigurationRoot Configuration { get; } - // This method gets called by the runtime. Use this method to add services to the container - public void ConfigureServices(IServiceCollection services) - { - services.AddControllers(); - - // Add framework services. - services.AddApplicationInsightsTelemetry(Configuration); - } + // This method gets called by the runtime. Use this method to add services to the container + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + + // Add framework services. + services.AddApplicationInsightsTelemetry(Configuration); + } - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) + { + if (env.IsDevelopment()) { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } + app.UseDeveloperExceptionPage(); + } - app.UseHttpsRedirection(); + app.UseHttpsRedirection(); - app.UseRouting(); + app.UseRouting(); - app.UseAuthorization(); + app.UseAuthorization(); - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); } } diff --git a/src/Markdig/Extensions/Abbreviations/Abbreviation.cs b/src/Markdig/Extensions/Abbreviations/Abbreviation.cs index 5543daa93..f04d0f77d 100644 --- a/src/Markdig/Extensions/Abbreviations/Abbreviation.cs +++ b/src/Markdig/Extensions/Abbreviations/Abbreviation.cs @@ -7,36 +7,35 @@ using Markdig.Parsers; using Markdig.Syntax; -namespace Markdig.Extensions.Abbreviations +namespace Markdig.Extensions.Abbreviations; + +/// +/// An abbreviation object stored at the document level. See extension methods in . +/// +/// +[DebuggerDisplay("Abbr {Label} => {Text}")] +public class Abbreviation : LeafBlock { /// - /// An abbreviation object stored at the document level. See extension methods in . + /// Initializes a new instance of the class. /// - /// - [DebuggerDisplay("Abbr {Label} => {Text}")] - public class Abbreviation : LeafBlock + /// The parser used to create this block. + public Abbreviation(BlockParser parser) : base(parser) { - /// - /// Initializes a new instance of the class. - /// - /// The parser used to create this block. - public Abbreviation(BlockParser parser) : base(parser) - { - } + } - /// - /// Gets or sets the label. - /// - public string? Label { get; set; } + /// + /// Gets or sets the label. + /// + public string? Label { get; set; } - /// - /// The text associated to this label. - /// - public StringSlice Text; + /// + /// The text associated to this label. + /// + public StringSlice Text; - /// - /// The label span - /// - public SourceSpan LabelSpan; - } + /// + /// The label span + /// + public SourceSpan LabelSpan; } \ No newline at end of file diff --git a/src/Markdig/Extensions/Abbreviations/AbbreviationExtension.cs b/src/Markdig/Extensions/Abbreviations/AbbreviationExtension.cs index e957c2f52..f21028f91 100644 --- a/src/Markdig/Extensions/Abbreviations/AbbreviationExtension.cs +++ b/src/Markdig/Extensions/Abbreviations/AbbreviationExtension.cs @@ -4,26 +4,25 @@ using Markdig.Renderers; -namespace Markdig.Extensions.Abbreviations +namespace Markdig.Extensions.Abbreviations; + +/// +/// Extension to allow abbreviations. +/// +/// +public class AbbreviationExtension : IMarkdownExtension { - /// - /// Extension to allow abbreviations. - /// - /// - public class AbbreviationExtension : IMarkdownExtension + public void Setup(MarkdownPipelineBuilder pipeline) { - public void Setup(MarkdownPipelineBuilder pipeline) - { - pipeline.BlockParsers.AddIfNotAlready(); - } + pipeline.BlockParsers.AddIfNotAlready(); + } - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + if (renderer is HtmlRenderer htmlRenderer && !htmlRenderer.ObjectRenderers.Contains()) { - if (renderer is HtmlRenderer htmlRenderer && !htmlRenderer.ObjectRenderers.Contains()) - { - // Must be inserted before CodeBlockRenderer - htmlRenderer.ObjectRenderers.Insert(0, new HtmlAbbreviationRenderer()); - } + // Must be inserted before CodeBlockRenderer + htmlRenderer.ObjectRenderers.Insert(0, new HtmlAbbreviationRenderer()); } } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Abbreviations/AbbreviationHelper.cs b/src/Markdig/Extensions/Abbreviations/AbbreviationHelper.cs index 8e0fa3e79..aefeca6ff 100644 --- a/src/Markdig/Extensions/Abbreviations/AbbreviationHelper.cs +++ b/src/Markdig/Extensions/Abbreviations/AbbreviationHelper.cs @@ -2,42 +2,40 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using System.Collections.Generic; using Markdig.Helpers; using Markdig.Syntax; -namespace Markdig.Extensions.Abbreviations +namespace Markdig.Extensions.Abbreviations; + +/// +/// Extension methods for . +/// +public static class AbbreviationHelper { - /// - /// Extension methods for . - /// - public static class AbbreviationHelper + private static readonly object DocumentKey = typeof (Abbreviation); + + public static bool HasAbbreviations(this MarkdownDocument document) { - private static readonly object DocumentKey = typeof (Abbreviation); + return document.GetAbbreviations() != null; + } - public static bool HasAbbreviations(this MarkdownDocument document) - { - return document.GetAbbreviations() != null; - } + public static void AddAbbreviation(this MarkdownDocument document, string label, Abbreviation abbr) + { + if (document is null) ThrowHelper.ArgumentNullException(nameof(document)); + if (label is null) ThrowHelper.ArgumentNullException_label(); + if (abbr is null) ThrowHelper.ArgumentNullException(nameof(abbr)); - public static void AddAbbreviation(this MarkdownDocument document, string label, Abbreviation abbr) + var map = document.GetAbbreviations(); + if (map is null) { - if (document is null) ThrowHelper.ArgumentNullException(nameof(document)); - if (label is null) ThrowHelper.ArgumentNullException_label(); - if (abbr is null) ThrowHelper.ArgumentNullException(nameof(abbr)); - - var map = document.GetAbbreviations(); - if (map is null) - { - map = new Dictionary(); - document.SetData(DocumentKey, map); - } - map[label] = abbr; + map = new Dictionary(); + document.SetData(DocumentKey, map); } + map[label] = abbr; + } - public static Dictionary? GetAbbreviations(this MarkdownDocument document) - { - return document.GetData(DocumentKey) as Dictionary; - } + public static Dictionary? GetAbbreviations(this MarkdownDocument document) + { + return document.GetData(DocumentKey) as Dictionary; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Abbreviations/AbbreviationInline.cs b/src/Markdig/Extensions/Abbreviations/AbbreviationInline.cs index 75678214c..e3108f2e6 100644 --- a/src/Markdig/Extensions/Abbreviations/AbbreviationInline.cs +++ b/src/Markdig/Extensions/Abbreviations/AbbreviationInline.cs @@ -5,24 +5,23 @@ using System.Diagnostics; using Markdig.Syntax.Inlines; -namespace Markdig.Extensions.Abbreviations +namespace Markdig.Extensions.Abbreviations; + +/// +/// The inline abbreviation. +/// +/// +[DebuggerDisplay("{Abbreviation}")] +public class AbbreviationInline : LeafInline { /// - /// The inline abbreviation. + /// Initializes a new instance of the class. /// - /// - [DebuggerDisplay("{Abbreviation}")] - public class AbbreviationInline : LeafInline + /// The abbreviation. + public AbbreviationInline(Abbreviation abbreviation) { - /// - /// Initializes a new instance of the class. - /// - /// The abbreviation. - public AbbreviationInline(Abbreviation abbreviation) - { - Abbreviation = abbreviation; - } - - public Abbreviation Abbreviation { get; set; } + Abbreviation = abbreviation; } + + public Abbreviation Abbreviation { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Abbreviations/AbbreviationParser.cs b/src/Markdig/Extensions/Abbreviations/AbbreviationParser.cs index 56cd07be3..9c451eac7 100644 --- a/src/Markdig/Extensions/Abbreviations/AbbreviationParser.cs +++ b/src/Markdig/Extensions/Abbreviations/AbbreviationParser.cs @@ -2,225 +2,222 @@ // 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.Collections.Generic; using Markdig.Helpers; using Markdig.Parsers; using Markdig.Syntax; using Markdig.Syntax.Inlines; -namespace Markdig.Extensions.Abbreviations +namespace Markdig.Extensions.Abbreviations; + +/// +/// A block parser for abbreviations. +/// +/// +public class AbbreviationParser : BlockParser { /// - /// A block parser for abbreviations. + /// Initializes a new instance of the class. /// - /// - public class AbbreviationParser : BlockParser + public AbbreviationParser() + { + OpeningCharacters = new[] { '*' }; + } + + public override BlockState TryOpen(BlockProcessor processor) { - /// - /// Initializes a new instance of the class. - /// - public AbbreviationParser() + if (processor.IsCodeIndent) { - OpeningCharacters = new[] { '*' }; + return BlockState.None; } - public override BlockState TryOpen(BlockProcessor processor) + // A link must be of the form *[Some Text]: An abbreviation + var slice = processor.Line; + var startPosition = slice.Start; + var c = slice.NextChar(); + if (c != '[') { - if (processor.IsCodeIndent) - { - return BlockState.None; - } + return BlockState.None; + } - // A link must be of the form *[Some Text]: An abbreviation - var slice = processor.Line; - var startPosition = slice.Start; - var c = slice.NextChar(); - if (c != '[') - { - return BlockState.None; - } + if (!LinkHelper.TryParseLabel(ref slice, out string? label, out SourceSpan labelSpan)) + { + return BlockState.None; + } - if (!LinkHelper.TryParseLabel(ref slice, out string? label, out SourceSpan labelSpan)) - { - return BlockState.None; - } + c = slice.CurrentChar; + if (c != ':') + { + return BlockState.None; + } + slice.SkipChar(); - c = slice.CurrentChar; - if (c != ':') - { - return BlockState.None; - } - slice.SkipChar(); + slice.Trim(); - slice.Trim(); + var abbr = new Abbreviation(this) + { + Label = label, + Text = slice, + Span = new SourceSpan(startPosition, slice.End), + Line = processor.LineIndex, + Column = processor.Column, + LabelSpan = labelSpan, + }; + if (!processor.Document.HasAbbreviations()) + { + processor.Document.ProcessInlinesBegin += DocumentOnProcessInlinesBegin; + } + processor.Document.AddAbbreviation(abbr.Label, abbr); - var abbr = new Abbreviation(this) - { - Label = label, - Text = slice, - Span = new SourceSpan(startPosition, slice.End), - Line = processor.LineIndex, - Column = processor.Column, - LabelSpan = labelSpan, - }; - if (!processor.Document.HasAbbreviations()) - { - processor.Document.ProcessInlinesBegin += DocumentOnProcessInlinesBegin; - } - processor.Document.AddAbbreviation(abbr.Label, abbr); + return BlockState.BreakDiscard; + } - return BlockState.BreakDiscard; - } + private void DocumentOnProcessInlinesBegin(InlineProcessor inlineProcessor, Inline? inline) + { + inlineProcessor.Document.ProcessInlinesBegin -= DocumentOnProcessInlinesBegin; - private void DocumentOnProcessInlinesBegin(InlineProcessor inlineProcessor, Inline? inline) + var abbreviations = inlineProcessor.Document.GetAbbreviations(); + // Should not happen, but another extension could decide to remove them, so... + if (abbreviations is null) { - inlineProcessor.Document.ProcessInlinesBegin -= DocumentOnProcessInlinesBegin; - - var abbreviations = inlineProcessor.Document.GetAbbreviations(); - // Should not happen, but another extension could decide to remove them, so... - if (abbreviations is null) - { - return; - } + return; + } - // Build a text matcher from the abbreviations labels - var prefixTree = new CompactPrefixTree(abbreviations); + // Build a text matcher from the abbreviations labels + var prefixTree = new CompactPrefixTree(abbreviations); - inlineProcessor.LiteralInlineParser.PostMatch += (InlineProcessor processor, ref StringSlice slice) => - { - var literal = (LiteralInline)processor.Inline!; - var originalLiteral = literal; + inlineProcessor.LiteralInlineParser.PostMatch += (InlineProcessor processor, ref StringSlice slice) => + { + var literal = (LiteralInline)processor.Inline!; + var originalLiteral = literal; - ContainerInline? container = null; + ContainerInline? container = null; - // This is slow, but we don't have much the choice - var content = literal.Content; - var text = content.Text; + // This is slow, but we don't have much the choice + var content = literal.Content; + var text = content.Text; - for (int i = content.Start; i <= content.End; i++) + for (int i = content.Start; i <= content.End; i++) + { + // Abbreviation must be a whole word == start at the start of a line or after a whitespace + if (i != 0) { - // Abbreviation must be a whole word == start at the start of a line or after a whitespace - if (i != 0) + for (i = i - 1; i <= content.End; i++) { - for (i = i - 1; i <= content.End; i++) + if (text[i].IsWhitespace()) { - if (text[i].IsWhitespace()) - { - i++; - goto ValidAbbreviationStart; - } + i++; + goto ValidAbbreviationStart; } - break; } + break; + } - ValidAbbreviationStart:; + ValidAbbreviationStart:; - if (prefixTree.TryMatchLongest(text.AsSpan(i, content.End - i + 1), out KeyValuePair abbreviationMatch)) + if (prefixTree.TryMatchLongest(text.AsSpan(i, content.End - i + 1), out KeyValuePair abbreviationMatch)) + { + var match = abbreviationMatch.Key; + if (!IsValidAbbreviationEnding(match, content, i)) { - var match = abbreviationMatch.Key; - if (!IsValidAbbreviationEnding(match, content, i)) - { - continue; - } - - var indexAfterMatch = i + match.Length; + continue; + } - // If we don't have a container, create a new one - if (container is null) - { - container = literal.Parent ?? - new ContainerInline - { - Span = originalLiteral.Span, - Line = originalLiteral.Line, - Column = originalLiteral.Column, - }; - } + var indexAfterMatch = i + match.Length; - var abbrInline = new AbbreviationInline(abbreviationMatch.Value) - { - Span = + // If we don't have a container, create a new one + if (container is null) + { + container = literal.Parent ?? + new ContainerInline { - Start = processor.GetSourcePosition(i, out int line, out int column), - }, - Line = line, - Column = column - }; - abbrInline.Span.End = abbrInline.Span.Start + match.Length - 1; - - // Append the previous literal - if (i > content.Start && literal.Parent is null) - { - container.AppendChild(literal); - } + Span = originalLiteral.Span, + Line = originalLiteral.Line, + Column = originalLiteral.Column, + }; + } - literal.Span.End = abbrInline.Span.Start - 1; - // Truncate it before the abbreviation - literal.Content.End = i - 1; + var abbrInline = new AbbreviationInline(abbreviationMatch.Value) + { + Span = + { + Start = processor.GetSourcePosition(i, out int line, out int column), + }, + Line = line, + Column = column + }; + abbrInline.Span.End = abbrInline.Span.Start + match.Length - 1; + + // Append the previous literal + if (i > content.Start && literal.Parent is null) + { + container.AppendChild(literal); + } + literal.Span.End = abbrInline.Span.Start - 1; + // Truncate it before the abbreviation + literal.Content.End = i - 1; - // Append the abbreviation - container.AppendChild(abbrInline); - // If this is the end of the string, clear the literal and exit - if (content.End == indexAfterMatch - 1) - { - literal = null; - break; - } + // Append the abbreviation + container.AppendChild(abbrInline); - // Process the remaining literal - literal = new LiteralInline() - { - Span = new SourceSpan(abbrInline.Span.End + 1, literal.Span.End), - Line = line, - Column = column + match.Length, - }; - content.Start = indexAfterMatch; - literal.Content = content; - - i = indexAfterMatch - 1; + // If this is the end of the string, clear the literal and exit + if (content.End == indexAfterMatch - 1) + { + literal = null; + break; } - } - if (container != null) - { - if (literal != null) + // Process the remaining literal + literal = new LiteralInline() { - container.AppendChild(literal); - } - processor.Inline = container; + Span = new SourceSpan(abbrInline.Span.End + 1, literal.Span.End), + Line = line, + Column = column + match.Length, + }; + content.Start = indexAfterMatch; + literal.Content = content; + + i = indexAfterMatch - 1; } - }; - } + } - private static bool IsValidAbbreviationEnding(string match, StringSlice content, int matchIndex) - { - // This will check if the next char at the end of the StringSlice is whitespace, punctuation or \0. - var contentNew = content; - contentNew.End = content.End + 1; - int index = matchIndex + match.Length; - while (index <= contentNew.End) + if (container != null) { - var c = contentNew.PeekCharAbsolute(index); - if (!(c == '\0' || c.IsWhitespace() || c.IsAsciiPunctuation())) + if (literal != null) { - return false; + container.AppendChild(literal); } + processor.Inline = container; + } + }; + } - if (c.IsAlphaNumeric()) - { - return false; - } + private static bool IsValidAbbreviationEnding(string match, StringSlice content, int matchIndex) + { + // This will check if the next char at the end of the StringSlice is whitespace, punctuation or \0. + var contentNew = content; + contentNew.End = content.End + 1; + int index = matchIndex + match.Length; + while (index <= contentNew.End) + { + var c = contentNew.PeekCharAbsolute(index); + if (!(c == '\0' || c.IsWhitespace() || c.IsAsciiPunctuation())) + { + return false; + } - if (c.IsWhitespace()) - { - break; - } - index++; + if (c.IsAlphaNumeric()) + { + return false; + } + + if (c.IsWhitespace()) + { + break; } - return true; + index++; } + return true; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Abbreviations/HtmlAbbreviationRenderer.cs b/src/Markdig/Extensions/Abbreviations/HtmlAbbreviationRenderer.cs index bfe3d086a..30ad8c178 100644 --- a/src/Markdig/Extensions/Abbreviations/HtmlAbbreviationRenderer.cs +++ b/src/Markdig/Extensions/Abbreviations/HtmlAbbreviationRenderer.cs @@ -5,27 +5,26 @@ using Markdig.Renderers; using Markdig.Renderers.Html; -namespace Markdig.Extensions.Abbreviations +namespace Markdig.Extensions.Abbreviations; + +/// +/// A HTML renderer for a . +/// +/// +public class HtmlAbbreviationRenderer : HtmlObjectRenderer { - /// - /// A HTML renderer for a . - /// - /// - public class HtmlAbbreviationRenderer : HtmlObjectRenderer + protected override void Write(HtmlRenderer renderer, AbbreviationInline obj) { - protected override void Write(HtmlRenderer renderer, AbbreviationInline obj) + // HTML + var abbr = obj.Abbreviation; + if (renderer.EnableHtmlForInline) + { + renderer.Write(""); + } + renderer.Write(abbr.Label); + if (renderer.EnableHtmlForInline) { - // HTML - var abbr = obj.Abbreviation; - if (renderer.EnableHtmlForInline) - { - renderer.Write(""); - } - renderer.Write(abbr.Label); - if (renderer.EnableHtmlForInline) - { - renderer.Write(""); - } + renderer.Write(""); } } } \ No newline at end of file diff --git a/src/Markdig/Extensions/AutoIdentifiers/AutoIdentifierExtension.cs b/src/Markdig/Extensions/AutoIdentifiers/AutoIdentifierExtension.cs index b35fd9ec5..a7fb77dd7 100644 --- a/src/Markdig/Extensions/AutoIdentifiers/AutoIdentifierExtension.cs +++ b/src/Markdig/Extensions/AutoIdentifiers/AutoIdentifierExtension.cs @@ -2,8 +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.Collections.Generic; using System.IO; + using Markdig.Helpers; using Markdig.Parsers; using Markdig.Renderers; @@ -11,206 +11,205 @@ using Markdig.Syntax; using Markdig.Syntax.Inlines; -namespace Markdig.Extensions.AutoIdentifiers +namespace Markdig.Extensions.AutoIdentifiers; + +/// +/// The auto-identifier extension +/// +/// +public class AutoIdentifierExtension : IMarkdownExtension { + private const string AutoIdentifierKey = "AutoIdentifier"; + private readonly AutoIdentifierOptions options; + private readonly StripRendererCache rendererCache = new StripRendererCache(); + /// - /// The auto-identifier extension + /// Initializes a new instance of the class. /// - /// - public class AutoIdentifierExtension : IMarkdownExtension + /// The options. + public AutoIdentifierExtension(AutoIdentifierOptions options) + { + this.options = options; + } + + public void Setup(MarkdownPipelineBuilder pipeline) { - private const string AutoIdentifierKey = "AutoIdentifier"; - private readonly AutoIdentifierOptions options; - private readonly StripRendererCache rendererCache = new StripRendererCache(); - - /// - /// Initializes a new instance of the class. - /// - /// The options. - public AutoIdentifierExtension(AutoIdentifierOptions options) + var headingBlockParser = pipeline.BlockParsers.Find(); + if (headingBlockParser != null) { - this.options = options; + // Install a hook on the HeadingBlockParser when a HeadingBlock is actually processed + headingBlockParser.Closed -= HeadingBlockParser_Closed; + headingBlockParser.Closed += HeadingBlockParser_Closed; } - - public void Setup(MarkdownPipelineBuilder pipeline) + var paragraphBlockParser = pipeline.BlockParsers.FindExact(); + if (paragraphBlockParser != null) { - var headingBlockParser = pipeline.BlockParsers.Find(); - if (headingBlockParser != null) - { - // Install a hook on the HeadingBlockParser when a HeadingBlock is actually processed - headingBlockParser.Closed -= HeadingBlockParser_Closed; - headingBlockParser.Closed += HeadingBlockParser_Closed; - } - var paragraphBlockParser = pipeline.BlockParsers.FindExact(); - if (paragraphBlockParser != null) - { - // Install a hook on the ParagraphBlockParser when a HeadingBlock is actually processed as a Setex heading - paragraphBlockParser.Closed -= HeadingBlockParser_Closed; - paragraphBlockParser.Closed += HeadingBlockParser_Closed; - } + // Install a hook on the ParagraphBlockParser when a HeadingBlock is actually processed as a Setex heading + paragraphBlockParser.Closed -= HeadingBlockParser_Closed; + paragraphBlockParser.Closed += HeadingBlockParser_Closed; } + } - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + } + + /// + /// Process on a new + /// + /// The processor. + /// The heading block. + private void HeadingBlockParser_Closed(BlockProcessor processor, Block block) + { + // We may have a ParagraphBlock here as we have a hook on the ParagraphBlockParser + if (!(block is HeadingBlock headingBlock)) { + return; } - /// - /// Process on a new - /// - /// The processor. - /// The heading block. - private void HeadingBlockParser_Closed(BlockProcessor processor, Block block) + // If the AutoLink options is set, we register a LinkReferenceDefinition at the document level + if ((options & AutoIdentifierOptions.AutoLink) != 0) { - // We may have a ParagraphBlock here as we have a hook on the ParagraphBlockParser - if (!(block is HeadingBlock headingBlock)) + var headingLine = headingBlock.Lines.Lines[0]; + + var text = headingLine.ToString(); + + var linkRef = new HeadingLinkReferenceDefinition(headingBlock) { - return; - } + CreateLinkInline = CreateLinkInlineForHeading + }; - // If the AutoLink options is set, we register a LinkReferenceDefinition at the document level - if ((options & AutoIdentifierOptions.AutoLink) != 0) + var doc = processor.Document; + var dictionary = doc.GetData(this) as Dictionary; + if (dictionary is null) { - var headingLine = headingBlock.Lines.Lines[0]; - - var text = headingLine.ToString(); - - var linkRef = new HeadingLinkReferenceDefinition(headingBlock) - { - CreateLinkInline = CreateLinkInlineForHeading - }; - - var doc = processor.Document; - var dictionary = doc.GetData(this) as Dictionary; - if (dictionary is null) - { - dictionary = new Dictionary(); - doc.SetData(this, dictionary); - doc.ProcessInlinesBegin += DocumentOnProcessInlinesBegin; - } - dictionary[text] = linkRef; + dictionary = new Dictionary(); + doc.SetData(this, dictionary); + doc.ProcessInlinesBegin += DocumentOnProcessInlinesBegin; } - - // Then we register after inline have been processed to actually generate the proper #id - headingBlock.ProcessInlinesEnd += HeadingBlock_ProcessInlinesEnd; + dictionary[text] = linkRef; } - private void DocumentOnProcessInlinesBegin(InlineProcessor processor, Inline? inline) + // Then we register after inline have been processed to actually generate the proper #id + headingBlock.ProcessInlinesEnd += HeadingBlock_ProcessInlinesEnd; + } + + private void DocumentOnProcessInlinesBegin(InlineProcessor processor, Inline? inline) + { + var doc = processor.Document; + doc.ProcessInlinesBegin -= DocumentOnProcessInlinesBegin; + var dictionary = (Dictionary)doc.GetData(this)!; + foreach (var keyPair in dictionary) { - var doc = processor.Document; - doc.ProcessInlinesBegin -= DocumentOnProcessInlinesBegin; - var dictionary = (Dictionary)doc.GetData(this)!; - foreach (var keyPair in dictionary) + // Here we make sure that auto-identifiers will not override an existing link definition + // defined in the document + // If it is the case, we skip the auto identifier for the Heading + if (!doc.TryGetLinkReferenceDefinition(keyPair.Key, out var linkDef)) { - // Here we make sure that auto-identifiers will not override an existing link definition - // defined in the document - // If it is the case, we skip the auto identifier for the Heading - if (!doc.TryGetLinkReferenceDefinition(keyPair.Key, out var linkDef)) - { - doc.SetLinkReferenceDefinition(keyPair.Key, keyPair.Value, true); - } + doc.SetLinkReferenceDefinition(keyPair.Key, keyPair.Value, true); } - // Once we are done, we don't need to keep the intermediate dictionary around - doc.RemoveData(this); } + // Once we are done, we don't need to keep the intermediate dictionary around + doc.RemoveData(this); + } - /// - /// Callback when there is a reference to found to a heading. - /// Note that reference are only working if they are declared after. - /// - private Inline CreateLinkInlineForHeading(InlineProcessor inlineState, LinkReferenceDefinition linkRef, Inline? child) + /// + /// Callback when there is a reference to found to a heading. + /// Note that reference are only working if they are declared after. + /// + private Inline CreateLinkInlineForHeading(InlineProcessor inlineState, LinkReferenceDefinition linkRef, Inline? child) + { + var headingRef = (HeadingLinkReferenceDefinition) linkRef; + return new LinkInline() { - var headingRef = (HeadingLinkReferenceDefinition) linkRef; - return new LinkInline() - { - // Use GetDynamicUrl to allow late binding of the Url (as a link may occur before the heading is declared and - // the inlines of the heading are actually processed by HeadingBlock_ProcessInlinesEnd) - GetDynamicUrl = () => HtmlHelper.Unescape("#" + headingRef.Heading.GetAttributes().Id), - Title = HtmlHelper.Unescape(linkRef.Title), - }; - } + // Use GetDynamicUrl to allow late binding of the Url (as a link may occur before the heading is declared and + // the inlines of the heading are actually processed by HeadingBlock_ProcessInlinesEnd) + GetDynamicUrl = () => HtmlHelper.Unescape("#" + headingRef.Heading.GetAttributes().Id), + Title = HtmlHelper.Unescape(linkRef.Title), + }; + } - /// - /// Process the inlines of the heading to create a unique identifier - /// - /// The processor. - /// The inline. - private void HeadingBlock_ProcessInlinesEnd(InlineProcessor processor, Inline? inline) + /// + /// Process the inlines of the heading to create a unique identifier + /// + /// The processor. + /// The inline. + private void HeadingBlock_ProcessInlinesEnd(InlineProcessor processor, Inline? inline) + { + var identifiers = processor.Document.GetData(AutoIdentifierKey) as HashSet; + if (identifiers is null) { - var identifiers = processor.Document.GetData(AutoIdentifierKey) as HashSet; - if (identifiers is null) - { - identifiers = new HashSet(); - processor.Document.SetData(AutoIdentifierKey, identifiers); - } + identifiers = new HashSet(); + processor.Document.SetData(AutoIdentifierKey, identifiers); + } - var headingBlock = (HeadingBlock) processor.Block!; - if (headingBlock.Inline is null) - { - return; - } + var headingBlock = (HeadingBlock) processor.Block!; + if (headingBlock.Inline is null) + { + return; + } - // If id is already set, don't try to modify it - var attributes = processor.Block!.GetAttributes(); - if (attributes.Id != null) - { - return; - } + // If id is already set, don't try to modify it + var attributes = processor.Block!.GetAttributes(); + if (attributes.Id != null) + { + return; + } - // Use internally a HtmlRenderer to strip links from a heading - var stripRenderer = rendererCache.Get(); + // Use internally a HtmlRenderer to strip links from a heading + var stripRenderer = rendererCache.Get(); - stripRenderer.Render(headingBlock.Inline); - var headingText = stripRenderer.Writer.ToString()!; - rendererCache.Release(stripRenderer); + stripRenderer.Render(headingBlock.Inline); + var headingText = stripRenderer.Writer.ToString()!; + rendererCache.Release(stripRenderer); - // Urilize the link - headingText = (options & AutoIdentifierOptions.GitHub) != 0 - ? LinkHelper.UrilizeAsGfm(headingText) - : LinkHelper.Urilize(headingText, (options & AutoIdentifierOptions.AllowOnlyAscii) != 0); + // Urilize the link + headingText = (options & AutoIdentifierOptions.GitHub) != 0 + ? LinkHelper.UrilizeAsGfm(headingText) + : LinkHelper.Urilize(headingText, (options & AutoIdentifierOptions.AllowOnlyAscii) != 0); - // If the heading is empty, use the word "section" instead - var baseHeadingId = string.IsNullOrEmpty(headingText) ? "section" : headingText; + // If the heading is empty, use the word "section" instead + var baseHeadingId = string.IsNullOrEmpty(headingText) ? "section" : headingText; - // Add a trailing -1, -2, -3...etc. in case of collision - var headingId = baseHeadingId; - if (!identifiers.Add(headingId)) + // Add a trailing -1, -2, -3...etc. in case of collision + var headingId = baseHeadingId; + if (!identifiers.Add(headingId)) + { + var headingBuffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); + headingBuffer.Append(baseHeadingId); + headingBuffer.Append('-'); + uint index = 0; + do { - var headingBuffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); - headingBuffer.Append(baseHeadingId); - headingBuffer.Append('-'); - uint index = 0; - do - { - index++; - headingBuffer.Append(index); - headingId = headingBuffer.AsSpan().ToString(); - headingBuffer.Length = baseHeadingId.Length + 1; - } - while (!identifiers.Add(headingId)); - headingBuffer.Dispose(); + index++; + headingBuffer.Append(index); + headingId = headingBuffer.AsSpan().ToString(); + headingBuffer.Length = baseHeadingId.Length + 1; } - - attributes.Id = headingId; + while (!identifiers.Add(headingId)); + headingBuffer.Dispose(); } - private sealed class StripRendererCache : ObjectCache + attributes.Id = headingId; + } + + private sealed class StripRendererCache : ObjectCache + { + protected override HtmlRenderer NewInstance() { - protected override HtmlRenderer NewInstance() + var headingWriter = new StringWriter(); + var stripRenderer = new HtmlRenderer(headingWriter) { - var headingWriter = new StringWriter(); - var stripRenderer = new HtmlRenderer(headingWriter) - { - // Set to false both to avoid having any HTML tags in the output - EnableHtmlForInline = false, - EnableHtmlEscape = false - }; - return stripRenderer; - } + // Set to false both to avoid having any HTML tags in the output + EnableHtmlForInline = false, + EnableHtmlEscape = false + }; + return stripRenderer; + } - protected override void Reset(HtmlRenderer instance) - { - instance.Reset(); - } + protected override void Reset(HtmlRenderer instance) + { + instance.Reset(); } } } \ No newline at end of file diff --git a/src/Markdig/Extensions/AutoIdentifiers/AutoIdentifierOptions.cs b/src/Markdig/Extensions/AutoIdentifiers/AutoIdentifierOptions.cs index 9585324ed..711c5e6ce 100644 --- a/src/Markdig/Extensions/AutoIdentifiers/AutoIdentifierOptions.cs +++ b/src/Markdig/Extensions/AutoIdentifiers/AutoIdentifierOptions.cs @@ -2,39 +2,36 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using System; +namespace Markdig.Extensions.AutoIdentifiers; -namespace Markdig.Extensions.AutoIdentifiers +/// +/// Options for the . +/// +[Flags] +public enum AutoIdentifierOptions { /// - /// Options for the . + /// No options: does not apply any additional formatting and/or transformations. /// - [Flags] - public enum AutoIdentifierOptions - { - /// - /// No options: does not apply any additional formatting and/or transformations. - /// - None = 0, + None = 0, - /// - /// Default () - /// - Default = AutoLink | AllowOnlyAscii, + /// + /// Default () + /// + Default = AutoLink | AllowOnlyAscii, - /// - /// Allows to link to a header by using the same text as the header for the link label. Default is true - /// - AutoLink = 1, + /// + /// Allows to link to a header by using the same text as the header for the link label. Default is true + /// + AutoLink = 1, - /// - /// Allows only ASCII characters in the url (HTML 5 allows to have UTF8 characters). Default is true - /// - AllowOnlyAscii = 2, + /// + /// Allows only ASCII characters in the url (HTML 5 allows to have UTF8 characters). Default is true + /// + AllowOnlyAscii = 2, - /// - /// Renders auto identifiers like GitHub. - /// - GitHub = 4, - } + /// + /// Renders auto identifiers like GitHub. + /// + GitHub = 4, } diff --git a/src/Markdig/Extensions/AutoIdentifiers/HeadingLinkReferenceDefinition.cs b/src/Markdig/Extensions/AutoIdentifiers/HeadingLinkReferenceDefinition.cs index 76e7862fe..5c7bdb1ea 100644 --- a/src/Markdig/Extensions/AutoIdentifiers/HeadingLinkReferenceDefinition.cs +++ b/src/Markdig/Extensions/AutoIdentifiers/HeadingLinkReferenceDefinition.cs @@ -4,22 +4,21 @@ using Markdig.Syntax; -namespace Markdig.Extensions.AutoIdentifiers +namespace Markdig.Extensions.AutoIdentifiers; + +/// +/// A link reference definition to a stored at the level. +/// +/// +public class HeadingLinkReferenceDefinition : LinkReferenceDefinition { - /// - /// A link reference definition to a stored at the level. - /// - /// - public class HeadingLinkReferenceDefinition : LinkReferenceDefinition + public HeadingLinkReferenceDefinition(HeadingBlock headling) { - public HeadingLinkReferenceDefinition(HeadingBlock headling) - { - Heading = headling; - } - - /// - /// Gets or sets the heading related to this link reference definition. - /// - public HeadingBlock Heading { get; set; } + Heading = headling; } + + /// + /// Gets or sets the heading related to this link reference definition. + /// + public HeadingBlock Heading { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/AutoLinks/AutoLinkExtension.cs b/src/Markdig/Extensions/AutoLinks/AutoLinkExtension.cs index 7e70ba1a2..efb030a3c 100644 --- a/src/Markdig/Extensions/AutoLinks/AutoLinkExtension.cs +++ b/src/Markdig/Extensions/AutoLinks/AutoLinkExtension.cs @@ -5,32 +5,31 @@ using Markdig.Renderers; using Markdig.Syntax.Inlines; -namespace Markdig.Extensions.AutoLinks +namespace Markdig.Extensions.AutoLinks; + +/// +/// Extension to automatically create when a link url http: or mailto: is found. +/// +/// +public class AutoLinkExtension : IMarkdownExtension { - /// - /// Extension to automatically create when a link url http: or mailto: is found. - /// - /// - public class AutoLinkExtension : IMarkdownExtension - { - public readonly AutoLinkOptions Options; + public readonly AutoLinkOptions Options; - public AutoLinkExtension(AutoLinkOptions? options) - { - Options = options ?? new AutoLinkOptions(); - } + public AutoLinkExtension(AutoLinkOptions? options) + { + Options = options ?? new AutoLinkOptions(); + } - public void Setup(MarkdownPipelineBuilder pipeline) + public void Setup(MarkdownPipelineBuilder pipeline) + { + if (!pipeline.InlineParsers.Contains()) { - if (!pipeline.InlineParsers.Contains()) - { - // Insert the parser before any other parsers - pipeline.InlineParsers.Insert(0, new AutoLinkParser(Options)); - } + // Insert the parser before any other parsers + pipeline.InlineParsers.Insert(0, new AutoLinkParser(Options)); } + } - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) - { - } + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { } } \ No newline at end of file diff --git a/src/Markdig/Extensions/AutoLinks/AutoLinkOptions.cs b/src/Markdig/Extensions/AutoLinks/AutoLinkOptions.cs index 04224a9e6..307b216b9 100644 --- a/src/Markdig/Extensions/AutoLinks/AutoLinkOptions.cs +++ b/src/Markdig/Extensions/AutoLinks/AutoLinkOptions.cs @@ -2,25 +2,24 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -namespace Markdig.Extensions.AutoLinks +namespace Markdig.Extensions.AutoLinks; + +public class AutoLinkOptions { - public class AutoLinkOptions + public AutoLinkOptions() { - public AutoLinkOptions() - { - ValidPreviousCharacters = "*_~("; - } + ValidPreviousCharacters = "*_~("; + } - public string ValidPreviousCharacters { get; set; } + public string ValidPreviousCharacters { get; set; } - /// - /// Should the link open in a new window when clicked (false by default) - /// - public bool OpenInNewWindow { get; set; } + /// + /// Should the link open in a new window when clicked (false by default) + /// + public bool OpenInNewWindow { get; set; } - /// - /// Should a www link be prefixed with https:// instead of http:// (false by default) - /// - public bool UseHttpsForWWWLinks { get; set; } - } + /// + /// Should a www link be prefixed with https:// instead of http:// (false by default) + /// + public bool UseHttpsForWWWLinks { get; set; } } diff --git a/src/Markdig/Extensions/AutoLinks/AutoLinkParser.cs b/src/Markdig/Extensions/AutoLinks/AutoLinkParser.cs index 6a39a82a4..64a84e45d 100644 --- a/src/Markdig/Extensions/AutoLinks/AutoLinkParser.cs +++ b/src/Markdig/Extensions/AutoLinks/AutoLinkParser.cs @@ -2,281 +2,278 @@ // 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.Collections.Generic; using Markdig.Helpers; using Markdig.Parsers; using Markdig.Renderers.Html; using Markdig.Syntax.Inlines; -namespace Markdig.Extensions.AutoLinks +namespace Markdig.Extensions.AutoLinks; + +/// +/// The inline parser used to for autolinks. +/// +/// +public class AutoLinkParser : InlineParser { /// - /// The inline parser used to for autolinks. + /// Initializes a new instance of the class. /// - /// - public class AutoLinkParser : InlineParser + public AutoLinkParser(AutoLinkOptions options) { - /// - /// Initializes a new instance of the class. - /// - public AutoLinkParser(AutoLinkOptions options) + Options = options ?? throw new ArgumentNullException(nameof(options)); + + OpeningCharacters = new char[] { - Options = options ?? throw new ArgumentNullException(nameof(options)); + 'h', // for http:// and https:// + 'f', // for ftp:// + 'm', // for mailto: + 't', // for tel: + 'w', // for www. + }; - OpeningCharacters = new char[] - { - 'h', // for http:// and https:// - 'f', // for ftp:// - 'm', // for mailto: - 't', // for tel: - 'w', // for www. - }; + _listOfCharCache = new ListOfCharCache(); + } + + public readonly AutoLinkOptions Options; - _listOfCharCache = new ListOfCharCache(); + private readonly ListOfCharCache _listOfCharCache; + + public override bool Match(InlineProcessor processor, ref StringSlice slice) + { + // Previous char must be a whitespace or a punctuation + var previousChar = slice.PeekCharExtra(-1); + if (!previousChar.IsWhiteSpaceOrZero() && Options.ValidPreviousCharacters.IndexOf(previousChar) == -1) + { + return false; } - public readonly AutoLinkOptions Options; + var startPosition = slice.Start; + int domainOffset = 0; - private readonly ListOfCharCache _listOfCharCache; + var c = slice.CurrentChar; + // Precheck URL + switch (c) + { + case 'h': + if (slice.MatchLowercase("ttp://", 1)) + { + domainOffset = 7; // http:// + } + else if (slice.MatchLowercase("ttps://", 1)) + { + domainOffset = 8; // https:// + } + else return false; + break; + case 'f': + if (!slice.MatchLowercase("tp://", 1)) + { + return false; + } + domainOffset = 6; // ftp:// + break; + case 'm': + if (!slice.MatchLowercase("ailto:", 1)) + { + return false; + } + break; + case 't': + if (!slice.MatchLowercase("el:", 1)) + { + return false; + } + domainOffset = 4; + break; + case 'w': + if (!slice.MatchLowercase("ww.", 1)) // We won't match http:/www. or /www.xxx + { + return false; + } + domainOffset = 4; // www. + break; + } - public override bool Match(InlineProcessor processor, ref StringSlice slice) + List pendingEmphasis = _listOfCharCache.Get(); + try { - // Previous char must be a whitespace or a punctuation - var previousChar = slice.PeekCharExtra(-1); - if (!previousChar.IsWhiteSpaceOrZero() && Options.ValidPreviousCharacters.IndexOf(previousChar) == -1) + // Check that an autolink is possible in the current context + if (!IsAutoLinkValidInCurrentContext(processor, pendingEmphasis)) + { + return false; + } + + // Parse URL + if (!LinkHelper.TryParseUrl(ref slice, out string? link, out _, true)) { return false; } - var startPosition = slice.Start; - int domainOffset = 0; - var c = slice.CurrentChar; - // Precheck URL - switch (c) + // If we have any pending emphasis, remove any pending emphasis characters from the end of the link + if (pendingEmphasis.Count > 0) { - case 'h': - if (slice.MatchLowercase("ttp://", 1)) + for (int i = link.Length - 1; i >= 0; i--) + { + if (pendingEmphasis.Contains(link[i])) { - domainOffset = 7; // http:// + slice.Start--; } - else if (slice.MatchLowercase("ttps://", 1)) + else { - domainOffset = 8; // https:// + if (i < link.Length - 1) + { + link = link.Substring(0, i + 1); + } + break; } - else return false; - break; - case 'f': - if (!slice.MatchLowercase("tp://", 1)) + } + } + + // Post-check URL + switch (c) + { + case 'h': + if (string.Equals(link, "http://", StringComparison.OrdinalIgnoreCase) || + string.Equals(link, "https://", StringComparison.OrdinalIgnoreCase)) { return false; } - domainOffset = 6; // ftp:// break; - case 'm': - if (!slice.MatchLowercase("ailto:", 1)) + case 'f': + if (string.Equals(link, "ftp://", StringComparison.OrdinalIgnoreCase)) { return false; } break; case 't': - if (!slice.MatchLowercase("el:", 1)) + if (string.Equals(link, "tel", StringComparison.OrdinalIgnoreCase)) { return false; } - domainOffset = 4; break; - case 'w': - if (!slice.MatchLowercase("ww.", 1)) // We won't match http:/www. or /www.xxx + case 'm': + int atIndex = link.IndexOf('@'); + if (atIndex == -1 || + atIndex == 7) // mailto:@ - no email part { return false; } - domainOffset = 4; // www. + domainOffset = atIndex + 1; break; } - List pendingEmphasis = _listOfCharCache.Get(); - try + // Do not need to check if a telephone number is a valid domain + if (c != 't' && !LinkHelper.IsValidDomain(link, domainOffset)) { - // Check that an autolink is possible in the current context - if (!IsAutoLinkValidInCurrentContext(processor, pendingEmphasis)) - { - return false; - } - - // Parse URL - if (!LinkHelper.TryParseUrl(ref slice, out string? link, out _, true)) - { - return false; - } - + return false; + } - // If we have any pending emphasis, remove any pending emphasis characters from the end of the link - if (pendingEmphasis.Count > 0) + var inline = new LinkInline() + { + Span = { - for (int i = link.Length - 1; i >= 0; i--) - { - if (pendingEmphasis.Contains(link[i])) - { - slice.Start--; - } - else - { - if (i < link.Length - 1) - { - link = link.Substring(0, i + 1); - } - break; - } - } - } + Start = processor.GetSourcePosition(startPosition, out int line, out int column), + }, + Line = line, + Column = column, + Url = c == 'w' ? ((Options.UseHttpsForWWWLinks ? "https://" : "http://") + link) : link, + IsClosed = true, + IsAutoLink = true, + }; - // Post-check URL - switch (c) - { - case 'h': - if (string.Equals(link, "http://", StringComparison.OrdinalIgnoreCase) || - string.Equals(link, "https://", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - break; - case 'f': - if (string.Equals(link, "ftp://", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - break; - case 't': - if (string.Equals(link, "tel", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - break; - case 'm': - int atIndex = link.IndexOf('@'); - if (atIndex == -1 || - atIndex == 7) // mailto:@ - no email part - { - return false; - } - domainOffset = atIndex + 1; - break; - } + var skipFromBeginning = c == 'm' ? 7 : 0; // For mailto: skip "mailto:" for content + skipFromBeginning = c == 't' ? 4 : skipFromBeginning; // See above but for tel: - // Do not need to check if a telephone number is a valid domain - if (c != 't' && !LinkHelper.IsValidDomain(link, domainOffset)) - { - return false; - } + inline.Span.End = inline.Span.Start + link.Length - 1; + inline.UrlSpan = inline.Span; + inline.AppendChild(new LiteralInline() + { + Span = inline.Span, + Line = line, + Column = column, + Content = new StringSlice(slice.Text, startPosition + skipFromBeginning, startPosition + link.Length - 1), + IsClosed = true + }); + processor.Inline = inline; - var inline = new LinkInline() - { - Span = - { - Start = processor.GetSourcePosition(startPosition, out int line, out int column), - }, - Line = line, - Column = column, - Url = c == 'w' ? ((Options.UseHttpsForWWWLinks ? "https://" : "http://") + link) : link, - IsClosed = true, - IsAutoLink = true, - }; + if (Options.OpenInNewWindow) + { + inline.GetAttributes().AddPropertyIfNotExist("target", "_blank"); + } - var skipFromBeginning = c == 'm' ? 7 : 0; // For mailto: skip "mailto:" for content - skipFromBeginning = c == 't' ? 4 : skipFromBeginning; // See above but for tel: + return true; + } + finally + { + _listOfCharCache.Release(pendingEmphasis); + } + } - inline.Span.End = inline.Span.Start + link.Length - 1; - inline.UrlSpan = inline.Span; - inline.AppendChild(new LiteralInline() + private bool IsAutoLinkValidInCurrentContext(InlineProcessor processor, List pendingEmphasis) + { + // Case where there is a pending HtmlInline + var currentInline = processor.Inline; + while (currentInline != null) + { + if (currentInline is HtmlInline htmlInline) + { + // If we have a we don't expect nested + if (htmlInline.Tag.StartsWith(", we can't allow a link + if (htmlInline.Tag.StartsWith(" pendingEmphasis) + // Check that we don't have any pending brackets opened (where we could have a possible markdown link) + // NOTE: This assume that [ and ] are used for links, otherwise autolink will not work properly + currentInline = processor.Inline; + int countBrackets = 0; + while (currentInline != null) { - // Case where there is a pending HtmlInline - var currentInline = processor.Inline; - while (currentInline != null) + if (currentInline is LinkDelimiterInline linkDelimiterInline && linkDelimiterInline.IsActive) { - if (currentInline is HtmlInline htmlInline) + if (linkDelimiterInline.Type == DelimiterType.Open) { - // If we have a we don't expect nested - if (htmlInline.Tag.StartsWith(", we can't allow a link - if (htmlInline.Tag.StartsWith("> + return countBrackets <= 0; + } + + private sealed class ListOfCharCache : DefaultObjectCache> + { + protected override void Reset(List instance) { - protected override void Reset(List instance) - { - instance.Clear(); - } + instance.Clear(); } } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Bootstrap/BootstrapExtension.cs b/src/Markdig/Extensions/Bootstrap/BootstrapExtension.cs index 363fa399c..8344f0a3a 100644 --- a/src/Markdig/Extensions/Bootstrap/BootstrapExtension.cs +++ b/src/Markdig/Extensions/Bootstrap/BootstrapExtension.cs @@ -7,57 +7,56 @@ using Markdig.Syntax; using Markdig.Syntax.Inlines; -namespace Markdig.Extensions.Bootstrap +namespace Markdig.Extensions.Bootstrap; + +/// +/// Extension for tagging some HTML elements with bootstrap classes. +/// +/// +public class BootstrapExtension : IMarkdownExtension { - /// - /// Extension for tagging some HTML elements with bootstrap classes. - /// - /// - public class BootstrapExtension : IMarkdownExtension + public void Setup(MarkdownPipelineBuilder pipeline) { - public void Setup(MarkdownPipelineBuilder pipeline) - { - // Make sure we don't have a delegate twice - pipeline.DocumentProcessed -= PipelineOnDocumentProcessed; - pipeline.DocumentProcessed += PipelineOnDocumentProcessed; - } + // Make sure we don't have a delegate twice + pipeline.DocumentProcessed -= PipelineOnDocumentProcessed; + pipeline.DocumentProcessed += PipelineOnDocumentProcessed; + } - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) - { - } + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + } - private static void PipelineOnDocumentProcessed(MarkdownDocument document) + private static void PipelineOnDocumentProcessed(MarkdownDocument document) + { + foreach (var node in document.Descendants()) { - foreach (var node in document.Descendants()) + if (node.IsInline) { - if (node.IsInline) + if (node.IsContainerInline && node is LinkInline link && link.IsImage) { - if (node.IsContainerInline && node is LinkInline link && link.IsImage) - { - link.GetAttributes().AddClass("img-fluid"); - } + link.GetAttributes().AddClass("img-fluid"); } - else if (node.IsContainerBlock) + } + else if (node.IsContainerBlock) + { + if (node is Tables.Table) + { + node.GetAttributes().AddClass("table"); + } + else if (node is QuoteBlock) { - if (node is Tables.Table) - { - node.GetAttributes().AddClass("table"); - } - else if (node is QuoteBlock) - { - node.GetAttributes().AddClass("blockquote"); - } - else if (node is Figures.Figure) - { - node.GetAttributes().AddClass("figure"); - } + node.GetAttributes().AddClass("blockquote"); } - else + else if (node is Figures.Figure) + { + node.GetAttributes().AddClass("figure"); + } + } + else + { + if (node is Figures.FigureCaption) { - if (node is Figures.FigureCaption) - { - node.GetAttributes().AddClass("figure-caption"); - } + node.GetAttributes().AddClass("figure-caption"); } } } diff --git a/src/Markdig/Extensions/Citations/CitationExtension.cs b/src/Markdig/Extensions/Citations/CitationExtension.cs index 812950d9d..9c7b64d9a 100644 --- a/src/Markdig/Extensions/Citations/CitationExtension.cs +++ b/src/Markdig/Extensions/Citations/CitationExtension.cs @@ -8,42 +8,41 @@ using Markdig.Renderers.Html.Inlines; using Markdig.Syntax.Inlines; -namespace Markdig.Extensions.Citations +namespace Markdig.Extensions.Citations; + +/// +/// Extension for cite ""..."" +/// +/// +public class CitationExtension : IMarkdownExtension { - /// - /// Extension for cite ""..."" - /// - /// - public class CitationExtension : IMarkdownExtension + public void Setup(MarkdownPipelineBuilder pipeline) { - public void Setup(MarkdownPipelineBuilder pipeline) + var parser = pipeline.InlineParsers.FindExact(); + if (parser != null && !parser.HasEmphasisChar('"')) { - var parser = pipeline.InlineParsers.FindExact(); - if (parser != null && !parser.HasEmphasisChar('"')) - { - parser.EmphasisDescriptors.Add(new EmphasisDescriptor('"', 2, 2, false)); - } + parser.EmphasisDescriptors.Add(new EmphasisDescriptor('"', 2, 2, false)); } + } - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + if (renderer is HtmlRenderer htmlRenderer) { - if (renderer is HtmlRenderer htmlRenderer) + // Extend the rendering here. + var emphasisRenderer = renderer.ObjectRenderers.FindExact(); + if (emphasisRenderer != null) { - // Extend the rendering here. - var emphasisRenderer = renderer.ObjectRenderers.FindExact(); - if (emphasisRenderer != null) - { - // TODO: Use an ordered list instead as we don't know if this specific GetTag has been already added - var previousTag = emphasisRenderer.GetTag; - emphasisRenderer.GetTag = inline => GetTag(inline) ?? previousTag(inline); - } + // TODO: Use an ordered list instead as we don't know if this specific GetTag has been already added + var previousTag = emphasisRenderer.GetTag; + emphasisRenderer.GetTag = inline => GetTag(inline) ?? previousTag(inline); } } + } - private static string? GetTag(EmphasisInline emphasisInline) - { - Debug.Assert(emphasisInline.DelimiterCount <= 2); - return emphasisInline.DelimiterCount == 2 && emphasisInline.DelimiterChar == '"' ? "cite" : null; - } + private static string? GetTag(EmphasisInline emphasisInline) + { + Debug.Assert(emphasisInline.DelimiterCount <= 2); + return emphasisInline.DelimiterCount == 2 && emphasisInline.DelimiterChar == '"' ? "cite" : null; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/CustomContainers/CustomContainer.cs b/src/Markdig/Extensions/CustomContainers/CustomContainer.cs index 27eeb851a..bb893a5d6 100644 --- a/src/Markdig/Extensions/CustomContainers/CustomContainer.cs +++ b/src/Markdig/Extensions/CustomContainers/CustomContainer.cs @@ -6,57 +6,56 @@ using Markdig.Parsers; using Markdig.Syntax; -namespace Markdig.Extensions.CustomContainers +namespace Markdig.Extensions.CustomContainers; + +/// +/// A block custom container. +/// +/// +/// +public class CustomContainer : ContainerBlock, IFencedBlock { /// - /// A block custom container. + /// Initializes a new instance of the class. /// - /// - /// - public class CustomContainer : ContainerBlock, IFencedBlock + /// The parser used to create this block. + public CustomContainer(BlockParser parser) : base(parser) { - /// - /// Initializes a new instance of the class. - /// - /// The parser used to create this block. - public CustomContainer(BlockParser parser) : base(parser) - { - } + } - /// - public char FencedChar { get; set; } + /// + public char FencedChar { get; set; } - /// - public int OpeningFencedCharCount { get; set; } + /// + public int OpeningFencedCharCount { get; set; } - /// - public StringSlice TriviaAfterFencedChar { get; set; } + /// + public StringSlice TriviaAfterFencedChar { get; set; } - /// - public string? Info { get; set; } + /// + public string? Info { get; set; } - /// - public StringSlice UnescapedInfo { get; set; } + /// + public StringSlice UnescapedInfo { get; set; } - /// - public StringSlice TriviaAfterInfo { get; set; } + /// + public StringSlice TriviaAfterInfo { get; set; } - /// - public string? Arguments { get; set; } + /// + public string? Arguments { get; set; } - /// - public StringSlice UnescapedArguments { get; set; } + /// + public StringSlice UnescapedArguments { get; set; } - /// - public StringSlice TriviaAfterArguments { get; set; } + /// + public StringSlice TriviaAfterArguments { get; set; } - /// - public NewLine InfoNewLine { get; set; } + /// + public NewLine InfoNewLine { get; set; } - /// - public StringSlice TriviaBeforeClosingFence { get; set; } + /// + public StringSlice TriviaBeforeClosingFence { get; set; } - /// - public int ClosingFencedCharCount { get; set; } - } + /// + public int ClosingFencedCharCount { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/CustomContainers/CustomContainerExtension.cs b/src/Markdig/Extensions/CustomContainers/CustomContainerExtension.cs index 3f30fd68f..d07d4a4cb 100644 --- a/src/Markdig/Extensions/CustomContainers/CustomContainerExtension.cs +++ b/src/Markdig/Extensions/CustomContainers/CustomContainerExtension.cs @@ -5,54 +5,53 @@ using Markdig.Parsers.Inlines; using Markdig.Renderers; -namespace Markdig.Extensions.CustomContainers +namespace Markdig.Extensions.CustomContainers; + +/// +/// Extension to allow custom containers. +/// +/// +public class CustomContainerExtension : IMarkdownExtension { - /// - /// Extension to allow custom containers. - /// - /// - public class CustomContainerExtension : IMarkdownExtension + public void Setup(MarkdownPipelineBuilder pipeline) { - public void Setup(MarkdownPipelineBuilder pipeline) + if (!pipeline.BlockParsers.Contains()) { - if (!pipeline.BlockParsers.Contains()) - { - // Insert the parser before any other parsers - pipeline.BlockParsers.Insert(0, new CustomContainerParser()); - } + // Insert the parser before any other parsers + pipeline.BlockParsers.Insert(0, new CustomContainerParser()); + } - // Plug the inline parser for CustomContainerInline - var inlineParser = pipeline.InlineParsers.Find(); - if (inlineParser != null && !inlineParser.HasEmphasisChar(':')) + // Plug the inline parser for CustomContainerInline + var inlineParser = pipeline.InlineParsers.Find(); + if (inlineParser != null && !inlineParser.HasEmphasisChar(':')) + { + inlineParser.EmphasisDescriptors.Add(new EmphasisDescriptor(':', 2, 2, true)); + inlineParser.TryCreateEmphasisInlineList.Add((emphasisChar, delimiterCount) => { - inlineParser.EmphasisDescriptors.Add(new EmphasisDescriptor(':', 2, 2, true)); - inlineParser.TryCreateEmphasisInlineList.Add((emphasisChar, delimiterCount) => + if (delimiterCount == 2 && emphasisChar == ':') { - if (delimiterCount == 2 && emphasisChar == ':') - { - return new CustomContainerInline(); - } - return null; - }); - } + return new CustomContainerInline(); + } + return null; + }); } + } - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + if (renderer is HtmlRenderer htmlRenderer) { - if (renderer is HtmlRenderer htmlRenderer) + if (!htmlRenderer.ObjectRenderers.Contains()) { - if (!htmlRenderer.ObjectRenderers.Contains()) - { - // Must be inserted before CodeBlockRenderer - htmlRenderer.ObjectRenderers.Insert(0, new HtmlCustomContainerRenderer()); - } - if (!htmlRenderer.ObjectRenderers.Contains()) - { - // Must be inserted before EmphasisRenderer - htmlRenderer.ObjectRenderers.Insert(0, new HtmlCustomContainerInlineRenderer()); - } + // Must be inserted before CodeBlockRenderer + htmlRenderer.ObjectRenderers.Insert(0, new HtmlCustomContainerRenderer()); + } + if (!htmlRenderer.ObjectRenderers.Contains()) + { + // Must be inserted before EmphasisRenderer + htmlRenderer.ObjectRenderers.Insert(0, new HtmlCustomContainerInlineRenderer()); } - } + } } \ No newline at end of file diff --git a/src/Markdig/Extensions/CustomContainers/CustomContainerInline.cs b/src/Markdig/Extensions/CustomContainers/CustomContainerInline.cs index 118c73a92..344e8c97a 100644 --- a/src/Markdig/Extensions/CustomContainers/CustomContainerInline.cs +++ b/src/Markdig/Extensions/CustomContainers/CustomContainerInline.cs @@ -4,14 +4,13 @@ using Markdig.Syntax.Inlines; -namespace Markdig.Extensions.CustomContainers +namespace Markdig.Extensions.CustomContainers; + +/// +/// An inline custom container +/// +/// +/// +public class CustomContainerInline : EmphasisInline { - /// - /// An inline custom container - /// - /// - /// - public class CustomContainerInline : EmphasisInline - { - } } \ No newline at end of file diff --git a/src/Markdig/Extensions/CustomContainers/CustomContainerParser.cs b/src/Markdig/Extensions/CustomContainers/CustomContainerParser.cs index af730f035..f72038a08 100644 --- a/src/Markdig/Extensions/CustomContainers/CustomContainerParser.cs +++ b/src/Markdig/Extensions/CustomContainers/CustomContainerParser.cs @@ -4,28 +4,27 @@ using Markdig.Parsers; -namespace Markdig.Extensions.CustomContainers +namespace Markdig.Extensions.CustomContainers; + +/// +/// The block parser for a . +/// +/// +public class CustomContainerParser : FencedBlockParserBase { /// - /// The block parser for a . + /// Initializes a new instance of the class. /// - /// - public class CustomContainerParser : FencedBlockParserBase + public CustomContainerParser() { - /// - /// Initializes a new instance of the class. - /// - public CustomContainerParser() - { - OpeningCharacters = new [] {':'}; + OpeningCharacters = new [] {':'}; - // We don't need a prefix - InfoPrefix = null; - } + // We don't need a prefix + InfoPrefix = null; + } - protected override CustomContainer CreateFencedBlock(BlockProcessor processor) - { - return new CustomContainer(this); - } + protected override CustomContainer CreateFencedBlock(BlockProcessor processor) + { + return new CustomContainer(this); } } \ No newline at end of file diff --git a/src/Markdig/Extensions/CustomContainers/HtmlCustomContainerInlineRenderer.cs b/src/Markdig/Extensions/CustomContainers/HtmlCustomContainerInlineRenderer.cs index 74318e266..11e20ea34 100644 --- a/src/Markdig/Extensions/CustomContainers/HtmlCustomContainerInlineRenderer.cs +++ b/src/Markdig/Extensions/CustomContainers/HtmlCustomContainerInlineRenderer.cs @@ -5,19 +5,18 @@ using Markdig.Renderers; using Markdig.Renderers.Html; -namespace Markdig.Extensions.CustomContainers +namespace Markdig.Extensions.CustomContainers; + +/// +/// A HTML renderer for a . +/// +/// +public class HtmlCustomContainerInlineRenderer : HtmlObjectRenderer { - /// - /// A HTML renderer for a . - /// - /// - public class HtmlCustomContainerInlineRenderer : HtmlObjectRenderer + protected override void Write(HtmlRenderer renderer, CustomContainerInline obj) { - protected override void Write(HtmlRenderer renderer, CustomContainerInline obj) - { - renderer.Write("'); - renderer.WriteChildren(obj); - renderer.Write(""); - } + renderer.Write("'); + renderer.WriteChildren(obj); + renderer.Write(""); } } \ No newline at end of file diff --git a/src/Markdig/Extensions/CustomContainers/HtmlCustomContainerRenderer.cs b/src/Markdig/Extensions/CustomContainers/HtmlCustomContainerRenderer.cs index a04149a78..3767d6349 100644 --- a/src/Markdig/Extensions/CustomContainers/HtmlCustomContainerRenderer.cs +++ b/src/Markdig/Extensions/CustomContainers/HtmlCustomContainerRenderer.cs @@ -5,27 +5,26 @@ using Markdig.Renderers; using Markdig.Renderers.Html; -namespace Markdig.Extensions.CustomContainers +namespace Markdig.Extensions.CustomContainers; + +/// +/// A HTML renderer for a . +/// +/// +public class HtmlCustomContainerRenderer : HtmlObjectRenderer { - /// - /// A HTML renderer for a . - /// - /// - public class HtmlCustomContainerRenderer : HtmlObjectRenderer + protected override void Write(HtmlRenderer renderer, CustomContainer obj) { - protected override void Write(HtmlRenderer renderer, CustomContainer obj) + renderer.EnsureLine(); + if (renderer.EnableHtmlForBlock) + { + renderer.Write("'); + } + // We don't escape a CustomContainer + renderer.WriteChildren(obj); + if (renderer.EnableHtmlForBlock) { - renderer.EnsureLine(); - if (renderer.EnableHtmlForBlock) - { - renderer.Write("'); - } - // We don't escape a CustomContainer - renderer.WriteChildren(obj); - if (renderer.EnableHtmlForBlock) - { - renderer.WriteLine("
  6. "); - } + renderer.WriteLine("
    "); } } } \ No newline at end of file diff --git a/src/Markdig/Extensions/DefinitionLists/DefinitionItem.cs b/src/Markdig/Extensions/DefinitionLists/DefinitionItem.cs index 799f57255..7e542168f 100644 --- a/src/Markdig/Extensions/DefinitionLists/DefinitionItem.cs +++ b/src/Markdig/Extensions/DefinitionLists/DefinitionItem.cs @@ -5,26 +5,25 @@ using Markdig.Parsers; using Markdig.Syntax; -namespace Markdig.Extensions.DefinitionLists +namespace Markdig.Extensions.DefinitionLists; + +/// +/// A definition item contains zero to multiple +/// and definitions (any ) +/// +/// +public class DefinitionItem : ContainerBlock { /// - /// A definition item contains zero to multiple - /// and definitions (any ) + /// Initializes a new instance of the class. /// - /// - public class DefinitionItem : ContainerBlock + /// The parser used to create this block. + public DefinitionItem(BlockParser parser) : base(parser) { - /// - /// Initializes a new instance of the class. - /// - /// The parser used to create this block. - public DefinitionItem(BlockParser parser) : base(parser) - { - } - - /// - /// Gets or sets the opening character for this definition item (either `:` or `~`) - /// - public char OpeningCharacter { get; set; } } + + /// + /// Gets or sets the opening character for this definition item (either `:` or `~`) + /// + public char OpeningCharacter { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/DefinitionLists/DefinitionList.cs b/src/Markdig/Extensions/DefinitionLists/DefinitionList.cs index fb3b60237..cd75a6244 100644 --- a/src/Markdig/Extensions/DefinitionLists/DefinitionList.cs +++ b/src/Markdig/Extensions/DefinitionLists/DefinitionList.cs @@ -5,20 +5,19 @@ using Markdig.Parsers; using Markdig.Syntax; -namespace Markdig.Extensions.DefinitionLists +namespace Markdig.Extensions.DefinitionLists; + +/// +/// A definition list contains children. +/// +/// +public class DefinitionList : ContainerBlock { /// - /// A definition list contains children. + /// Initializes a new instance of the class. /// - /// - public class DefinitionList : ContainerBlock + /// The parser used to create this block. + public DefinitionList(BlockParser parser) : base(parser) { - /// - /// Initializes a new instance of the class. - /// - /// The parser used to create this block. - public DefinitionList(BlockParser parser) : base(parser) - { - } } } \ No newline at end of file diff --git a/src/Markdig/Extensions/DefinitionLists/DefinitionListExtension.cs b/src/Markdig/Extensions/DefinitionLists/DefinitionListExtension.cs index 78825c7ec..32ef30aaf 100644 --- a/src/Markdig/Extensions/DefinitionLists/DefinitionListExtension.cs +++ b/src/Markdig/Extensions/DefinitionLists/DefinitionListExtension.cs @@ -4,31 +4,30 @@ using Markdig.Renderers; -namespace Markdig.Extensions.DefinitionLists +namespace Markdig.Extensions.DefinitionLists; + +/// +/// Extension to allow definition lists +/// +/// +public class DefinitionListExtension : IMarkdownExtension { - /// - /// Extension to allow definition lists - /// - /// - public class DefinitionListExtension : IMarkdownExtension + public void Setup(MarkdownPipelineBuilder pipeline) { - public void Setup(MarkdownPipelineBuilder pipeline) + if (!pipeline.BlockParsers.Contains()) { - if (!pipeline.BlockParsers.Contains()) - { - // Insert the parser before any other parsers - pipeline.BlockParsers.Insert(0, new DefinitionListParser()); - } + // Insert the parser before any other parsers + pipeline.BlockParsers.Insert(0, new DefinitionListParser()); } + } - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + if (renderer is HtmlRenderer htmlRenderer) { - if (renderer is HtmlRenderer htmlRenderer) + if (!htmlRenderer.ObjectRenderers.Contains()) { - if (!htmlRenderer.ObjectRenderers.Contains()) - { - htmlRenderer.ObjectRenderers.Insert(0, new HtmlDefinitionListRenderer()); - } + htmlRenderer.ObjectRenderers.Insert(0, new HtmlDefinitionListRenderer()); } } } diff --git a/src/Markdig/Extensions/DefinitionLists/DefinitionListParser.cs b/src/Markdig/Extensions/DefinitionLists/DefinitionListParser.cs index 7541664ee..31d901db4 100644 --- a/src/Markdig/Extensions/DefinitionLists/DefinitionListParser.cs +++ b/src/Markdig/Extensions/DefinitionLists/DefinitionListParser.cs @@ -2,196 +2,194 @@ // 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 Markdig.Parsers; using Markdig.Syntax; -namespace Markdig.Extensions.DefinitionLists +namespace Markdig.Extensions.DefinitionLists; + +/// +/// The block parser for a . +/// +/// +public class DefinitionListParser : BlockParser { /// - /// The block parser for a . + /// Initializes a new instance of the class. /// - /// - public class DefinitionListParser : BlockParser + public DefinitionListParser() + { + OpeningCharacters = new [] {':', '~'}; + } + + public override BlockState TryOpen(BlockProcessor processor) { - /// - /// Initializes a new instance of the class. - /// - public DefinitionListParser() + var paragraphBlock = processor.LastBlock as ParagraphBlock; + if (processor.IsCodeIndent || paragraphBlock is null || paragraphBlock.LastLine - processor.LineIndex > 1) { - OpeningCharacters = new [] {':', '~'}; + return BlockState.None; } - public override BlockState TryOpen(BlockProcessor processor) - { - var paragraphBlock = processor.LastBlock as ParagraphBlock; - if (processor.IsCodeIndent || paragraphBlock is null || paragraphBlock.LastLine - processor.LineIndex > 1) - { - return BlockState.None; - } + var startPosition = processor.Start; - var startPosition = processor.Start; + var column = processor.ColumnBeforeIndent; + processor.NextChar(); + processor.ParseIndent(); + var delta = processor.Column - column; - var column = processor.ColumnBeforeIndent; - processor.NextChar(); - processor.ParseIndent(); - var delta = processor.Column - column; + // We expect to have a least + if (delta < 4) + { + // Return back to original position + processor.GoToColumn(column); + return BlockState.None; + } - // We expect to have a least - if (delta < 4) - { - // Return back to original position - processor.GoToColumn(column); - return BlockState.None; - } + if (delta > 4) + { + processor.GoToColumn(column + 4); + } - if (delta > 4) - { - processor.GoToColumn(column + 4); - } + var previousParent = paragraphBlock.Parent!; + var currentDefinitionList = GetCurrentDefinitionList(paragraphBlock, previousParent); - var previousParent = paragraphBlock.Parent!; - var currentDefinitionList = GetCurrentDefinitionList(paragraphBlock, previousParent); + processor.Discard(paragraphBlock); - processor.Discard(paragraphBlock); + // If the paragraph block was not part of the opened blocks, we need to remove it manually from its parent container + if (paragraphBlock.Parent != null) + { + paragraphBlock.Parent.Remove(paragraphBlock); + } - // If the paragraph block was not part of the opened blocks, we need to remove it manually from its parent container - if (paragraphBlock.Parent != null) + if (currentDefinitionList is null) + { + currentDefinitionList = new DefinitionList(this) { - paragraphBlock.Parent.Remove(paragraphBlock); - } + Span = new SourceSpan(paragraphBlock.Span.Start, processor.Line.End), + Column = paragraphBlock.Column, + Line = paragraphBlock.Line, + }; + previousParent.Add(currentDefinitionList); + } - if (currentDefinitionList is null) - { - currentDefinitionList = new DefinitionList(this) - { - Span = new SourceSpan(paragraphBlock.Span.Start, processor.Line.End), - Column = paragraphBlock.Column, - Line = paragraphBlock.Line, - }; - previousParent.Add(currentDefinitionList); - } + var definitionItem = new DefinitionItem(this) + { + Line = processor.LineIndex, + Column = column, + Span = new SourceSpan(startPosition, processor.Line.End), + OpeningCharacter = processor.CurrentChar, + }; - var definitionItem = new DefinitionItem(this) + for (int i = 0; i < paragraphBlock.Lines.Count; i++) + { + var line = paragraphBlock.Lines.Lines[i]; + var term = new DefinitionTerm(this) { - Line = processor.LineIndex, - Column = column, - Span = new SourceSpan(startPosition, processor.Line.End), - OpeningCharacter = processor.CurrentChar, + Column = paragraphBlock.Column, + Line = line.Line, + Span = new SourceSpan(paragraphBlock.Span.Start, paragraphBlock.Span.End), + IsOpen = false }; + term.AppendLine(ref line.Slice, line.Column, line.Line, line.Position, processor.TrackTrivia); + definitionItem.Add(term); + } + currentDefinitionList.Add(definitionItem); + processor.Open(definitionItem); - for (int i = 0; i < paragraphBlock.Lines.Count; i++) - { - var line = paragraphBlock.Lines.Lines[i]; - var term = new DefinitionTerm(this) - { - Column = paragraphBlock.Column, - Line = line.Line, - Span = new SourceSpan(paragraphBlock.Span.Start, paragraphBlock.Span.End), - IsOpen = false - }; - term.AppendLine(ref line.Slice, line.Column, line.Line, line.Position, processor.TrackTrivia); - definitionItem.Add(term); - } - currentDefinitionList.Add(definitionItem); - processor.Open(definitionItem); + // Update the end position + currentDefinitionList.UpdateSpanEnd(processor.Line.End); - // Update the end position - currentDefinitionList.UpdateSpanEnd(processor.Line.End); + return BlockState.Continue; + } - return BlockState.Continue; + private static DefinitionList? GetCurrentDefinitionList(ParagraphBlock paragraphBlock, ContainerBlock previousParent) + { + var index = previousParent.IndexOf(paragraphBlock) - 1; + if (index < 0) return null; + var lastBlock = previousParent[index]; + if (lastBlock is BlankLineBlock) + { + lastBlock = previousParent[index - 1]; + previousParent.RemoveAt(index); } + return lastBlock as DefinitionList; + } - private static DefinitionList? GetCurrentDefinitionList(ParagraphBlock paragraphBlock, ContainerBlock previousParent) + public override BlockState TryContinue(BlockProcessor processor, Block block) + { + var definitionItem = (DefinitionItem)block; + if (processor.IsCodeIndent) { - var index = previousParent.IndexOf(paragraphBlock) - 1; - if (index < 0) return null; - var lastBlock = previousParent[index]; - if (lastBlock is BlankLineBlock) - { - lastBlock = previousParent[index - 1]; - previousParent.RemoveAt(index); - } - return lastBlock as DefinitionList; + processor.GoToCodeIndent(); + return BlockState.Continue; } - public override BlockState TryContinue(BlockProcessor processor, Block block) - { - var definitionItem = (DefinitionItem)block; - if (processor.IsCodeIndent) - { - processor.GoToCodeIndent(); - return BlockState.Continue; - } + var list = (DefinitionList)definitionItem.Parent!; + var lastBlankLine = definitionItem.LastChild as BlankLineBlock; - var list = (DefinitionList)definitionItem.Parent!; - var lastBlankLine = definitionItem.LastChild as BlankLineBlock; + // Check if we have another definition list + if (Array.IndexOf(OpeningCharacters!, processor.CurrentChar) >= 0) + { + var startPosition = processor.Start; + var column = processor.ColumnBeforeIndent; + processor.NextChar(); + processor.ParseIndent(); + var delta = processor.Column - column; - // Check if we have another definition list - if (Array.IndexOf(OpeningCharacters!, processor.CurrentChar) >= 0) + // We expect to have a least + if (delta < 4) { - var startPosition = processor.Start; - var column = processor.ColumnBeforeIndent; - processor.NextChar(); - processor.ParseIndent(); - var delta = processor.Column - column; - - // We expect to have a least - if (delta < 4) - { - // Remove the blankline before breaking this definition item - if (lastBlankLine != null) - { - definitionItem.RemoveAt(definitionItem.Count - 1); - } - - list.Span.End = list.LastChild!.Span.End; - return BlockState.None; - } - - if (delta > 4) + // Remove the blankline before breaking this definition item + if (lastBlankLine != null) { - processor.GoToColumn(column + 4); + definitionItem.RemoveAt(definitionItem.Count - 1); } - processor.Close(definitionItem); - var nextDefinitionItem = new DefinitionItem(this) - { - Span = new SourceSpan(startPosition, processor.Line.End), - Line = processor.LineIndex, - Column = processor.Column, - OpeningCharacter = processor.CurrentChar, - }; - list.Add(nextDefinitionItem); - processor.Open(nextDefinitionItem); - - return BlockState.Continue; + list.Span.End = list.LastChild!.Span.End; + return BlockState.None; } - var isBreakable = definitionItem.LastChild?.IsBreakable ?? true; - if (processor.IsBlankLine) + if (delta > 4) { - if (lastBlankLine is null && isBreakable) - { - definitionItem.Add(new BlankLineBlock()); - } - return isBreakable ? BlockState.ContinueDiscard : BlockState.Continue; + processor.GoToColumn(column + 4); } - var paragraphBlock = definitionItem.LastChild as ParagraphBlock; - if (lastBlankLine is null && paragraphBlock != null) + processor.Close(definitionItem); + var nextDefinitionItem = new DefinitionItem(this) { - return BlockState.Continue; - } + Span = new SourceSpan(startPosition, processor.Line.End), + Line = processor.LineIndex, + Column = processor.Column, + OpeningCharacter = processor.CurrentChar, + }; + list.Add(nextDefinitionItem); + processor.Open(nextDefinitionItem); - // Remove the blankline before breaking this definition item - if (lastBlankLine != null) + return BlockState.Continue; + } + + var isBreakable = definitionItem.LastChild?.IsBreakable ?? true; + if (processor.IsBlankLine) + { + if (lastBlankLine is null && isBreakable) { - definitionItem.RemoveAt(definitionItem.Count - 1); + definitionItem.Add(new BlankLineBlock()); } + return isBreakable ? BlockState.ContinueDiscard : BlockState.Continue; + } - list.Span.End = list.LastChild!.Span.End; - return BlockState.Break; + var paragraphBlock = definitionItem.LastChild as ParagraphBlock; + if (lastBlankLine is null && paragraphBlock != null) + { + return BlockState.Continue; } + + // Remove the blankline before breaking this definition item + if (lastBlankLine != null) + { + definitionItem.RemoveAt(definitionItem.Count - 1); + } + + list.Span.End = list.LastChild!.Span.End; + return BlockState.Break; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/DefinitionLists/DefinitionTerm.cs b/src/Markdig/Extensions/DefinitionLists/DefinitionTerm.cs index 2a15e5f14..27c382ce3 100644 --- a/src/Markdig/Extensions/DefinitionLists/DefinitionTerm.cs +++ b/src/Markdig/Extensions/DefinitionLists/DefinitionTerm.cs @@ -5,21 +5,20 @@ using Markdig.Parsers; using Markdig.Syntax; -namespace Markdig.Extensions.DefinitionLists +namespace Markdig.Extensions.DefinitionLists; + +/// +/// A definition term contains a single line with the term to define. +/// +/// +public class DefinitionTerm : LeafBlock { /// - /// A definition term contains a single line with the term to define. + /// Initializes a new instance of the class. /// - /// - public class DefinitionTerm : LeafBlock + /// The parser used to create this block. + public DefinitionTerm(BlockParser parser) : base(parser) { - /// - /// Initializes a new instance of the class. - /// - /// The parser used to create this block. - public DefinitionTerm(BlockParser parser) : base(parser) - { - ProcessInlines = true; - } + ProcessInlines = true; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/DefinitionLists/HtmlDefinitionListRenderer.cs b/src/Markdig/Extensions/DefinitionLists/HtmlDefinitionListRenderer.cs index cccba4f15..f1c50b532 100644 --- a/src/Markdig/Extensions/DefinitionLists/HtmlDefinitionListRenderer.cs +++ b/src/Markdig/Extensions/DefinitionLists/HtmlDefinitionListRenderer.cs @@ -6,80 +6,79 @@ using Markdig.Renderers.Html; using Markdig.Syntax; -namespace Markdig.Extensions.DefinitionLists +namespace Markdig.Extensions.DefinitionLists; + +/// +/// A HTML renderer for , and . +/// +/// +public class HtmlDefinitionListRenderer : HtmlObjectRenderer { - /// - /// A HTML renderer for , and . - /// - /// - public class HtmlDefinitionListRenderer : HtmlObjectRenderer + protected override void Write(HtmlRenderer renderer, DefinitionList list) { - protected override void Write(HtmlRenderer renderer, DefinitionList list) + renderer.EnsureLine(); + renderer.Write("'); + foreach (var item in list) { - renderer.EnsureLine(); - renderer.Write("'); - foreach (var item in list) + bool hasOpendd = false; + var definitionItem = (DefinitionItem) item; + int countdd = 0; + bool lastWasSimpleParagraph = false; + for (int i = 0; i < definitionItem.Count; i++) { - bool hasOpendd = false; - var definitionItem = (DefinitionItem) item; - int countdd = 0; - bool lastWasSimpleParagraph = false; - for (int i = 0; i < definitionItem.Count; i++) + var definitionTermOrContent = definitionItem[i]; + var definitionTerm = definitionTermOrContent as DefinitionTerm; + if (definitionTerm != null) { - var definitionTermOrContent = definitionItem[i]; - var definitionTerm = definitionTermOrContent as DefinitionTerm; - if (definitionTerm != null) + if (hasOpendd) { - if (hasOpendd) + if (!lastWasSimpleParagraph) { - if (!lastWasSimpleParagraph) - { - renderer.EnsureLine(); - } - renderer.WriteLine(""); - lastWasSimpleParagraph = false; - hasOpendd = false; - countdd = 0; + renderer.EnsureLine(); } - renderer.Write("'); - renderer.WriteLeafInline(definitionTerm); - renderer.WriteLine(""); + renderer.WriteLine(""); + lastWasSimpleParagraph = false; + hasOpendd = false; + countdd = 0; } - else + renderer.Write("'); + renderer.WriteLeafInline(definitionTerm); + renderer.WriteLine(""); + } + else + { + if (!hasOpendd) { - if (!hasOpendd) - { - renderer.Write("'); - countdd = 0; - hasOpendd = true; - } + renderer.Write("'); + countdd = 0; + hasOpendd = true; + } - var nextTerm = i + 1 < definitionItem.Count ? definitionItem[i + 1] : null; - bool isSimpleParagraph = (nextTerm is null || nextTerm is DefinitionItem) && countdd == 0 && - definitionTermOrContent is ParagraphBlock; + var nextTerm = i + 1 < definitionItem.Count ? definitionItem[i + 1] : null; + bool isSimpleParagraph = (nextTerm is null || nextTerm is DefinitionItem) && countdd == 0 && + definitionTermOrContent is ParagraphBlock; - var saveImplicitParagraph = renderer.ImplicitParagraph; - if (isSimpleParagraph) - { - renderer.ImplicitParagraph = true; - lastWasSimpleParagraph = true; - } - renderer.Write(definitionTermOrContent); - renderer.ImplicitParagraph = saveImplicitParagraph; - countdd++; + var saveImplicitParagraph = renderer.ImplicitParagraph; + if (isSimpleParagraph) + { + renderer.ImplicitParagraph = true; + lastWasSimpleParagraph = true; } + renderer.Write(definitionTermOrContent); + renderer.ImplicitParagraph = saveImplicitParagraph; + countdd++; } - if (hasOpendd) + } + if (hasOpendd) + { + if (!lastWasSimpleParagraph) { - if (!lastWasSimpleParagraph) - { - renderer.EnsureLine(); - } - renderer.WriteLine(""); + renderer.EnsureLine(); } + renderer.WriteLine(""); } - renderer.EnsureLine(); - renderer.WriteLine(""); } + renderer.EnsureLine(); + renderer.WriteLine(""); } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Diagrams/DiagramExtension.cs b/src/Markdig/Extensions/Diagrams/DiagramExtension.cs index db9f00002..b04e889a1 100644 --- a/src/Markdig/Extensions/Diagrams/DiagramExtension.cs +++ b/src/Markdig/Extensions/Diagrams/DiagramExtension.cs @@ -5,27 +5,26 @@ using Markdig.Renderers; using Markdig.Renderers.Html; -namespace Markdig.Extensions.Diagrams +namespace Markdig.Extensions.Diagrams; + +/// +/// Extension to allow diagrams. +/// +/// +public class DiagramExtension : IMarkdownExtension { - /// - /// Extension to allow diagrams. - /// - /// - public class DiagramExtension : IMarkdownExtension + public void Setup(MarkdownPipelineBuilder pipeline) { - public void Setup(MarkdownPipelineBuilder pipeline) - { - } + } - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + if (renderer is HtmlRenderer htmlRenderer) { - if (renderer is HtmlRenderer htmlRenderer) - { - var codeRenderer = htmlRenderer.ObjectRenderers.FindExact()!; - // TODO: Add other well known diagram languages - codeRenderer.BlocksAsDiv.Add("mermaid"); - codeRenderer.BlocksAsDiv.Add("nomnoml"); - } + var codeRenderer = htmlRenderer.ObjectRenderers.FindExact()!; + // TODO: Add other well known diagram languages + codeRenderer.BlocksAsDiv.Add("mermaid"); + codeRenderer.BlocksAsDiv.Add("nomnoml"); } } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Emoji/EmojiExtension.cs b/src/Markdig/Extensions/Emoji/EmojiExtension.cs index b92b88eb3..0627e78a4 100644 --- a/src/Markdig/Extensions/Emoji/EmojiExtension.cs +++ b/src/Markdig/Extensions/Emoji/EmojiExtension.cs @@ -4,32 +4,31 @@ using Markdig.Renderers; -namespace Markdig.Extensions.Emoji +namespace Markdig.Extensions.Emoji; + +/// +/// Extension to allow emoji shortcodes and smileys replacement. +/// +/// +public class EmojiExtension : IMarkdownExtension { - /// - /// Extension to allow emoji shortcodes and smileys replacement. - /// - /// - public class EmojiExtension : IMarkdownExtension + public EmojiExtension(EmojiMapping emojiMapping) { - public EmojiExtension(EmojiMapping emojiMapping) - { - EmojiMapping = emojiMapping; - } + EmojiMapping = emojiMapping; + } - public EmojiMapping EmojiMapping { get; } + public EmojiMapping EmojiMapping { get; } - public void Setup(MarkdownPipelineBuilder pipeline) + public void Setup(MarkdownPipelineBuilder pipeline) + { + if (!pipeline.InlineParsers.Contains()) { - if (!pipeline.InlineParsers.Contains()) - { - // Insert the parser before any other parsers - pipeline.InlineParsers.Insert(0, new EmojiParser(EmojiMapping)); - } + // Insert the parser before any other parsers + pipeline.InlineParsers.Insert(0, new EmojiParser(EmojiMapping)); } + } - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) - { - } + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Emoji/EmojiInline.cs b/src/Markdig/Extensions/Emoji/EmojiInline.cs index f64ec7ed6..d8520cf54 100644 --- a/src/Markdig/Extensions/Emoji/EmojiInline.cs +++ b/src/Markdig/Extensions/Emoji/EmojiInline.cs @@ -5,35 +5,34 @@ using Markdig.Helpers; using Markdig.Syntax.Inlines; -namespace Markdig.Extensions.Emoji +namespace Markdig.Extensions.Emoji; + +/// +/// An emoji inline. +/// +/// +public class EmojiInline : LiteralInline { + // Inherit from LiteralInline so that rendering is already handled by default + /// - /// An emoji inline. + /// Initializes a new instance of the class. /// - /// - public class EmojiInline : LiteralInline + public EmojiInline() { - // Inherit from LiteralInline so that rendering is already handled by default - - /// - /// Initializes a new instance of the class. - /// - public EmojiInline() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The content. - public EmojiInline(string content) - { - Content = new StringSlice(content); - } + } - /// - /// Gets or sets the original match string (either an emoji shortcode or a text smiley) - /// - public string? Match { get; set; } + /// + /// Initializes a new instance of the class. + /// + /// The content. + public EmojiInline(string content) + { + Content = new StringSlice(content); } + + /// + /// Gets or sets the original match string (either an emoji shortcode or a text smiley) + /// + public string? Match { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Emoji/EmojiMapping.cs b/src/Markdig/Extensions/Emoji/EmojiMapping.cs index 89b4307c7..f20e1517a 100644 --- a/src/Markdig/Extensions/Emoji/EmojiMapping.cs +++ b/src/Markdig/Extensions/Emoji/EmojiMapping.cs @@ -2,1792 +2,1790 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using System.Collections.Generic; using Markdig.Helpers; -namespace Markdig.Extensions.Emoji +namespace Markdig.Extensions.Emoji; + +/// +/// An emoji shortcodes and smileys mapping, to be used by . +/// +public class EmojiMapping { /// - /// An emoji shortcodes and smileys mapping, to be used by . + /// The default emoji shortcodes and smileys mapping. /// - public class EmojiMapping - { - /// - /// The default emoji shortcodes and smileys mapping. - /// - public static readonly EmojiMapping DefaultEmojisAndSmileysMapping = new EmojiMapping(); + public static readonly EmojiMapping DefaultEmojisAndSmileysMapping = new EmojiMapping(); - /// - /// The default emoji shortcodes mapping, without smileys. - /// - public static readonly EmojiMapping DefaultEmojisOnlyMapping = new EmojiMapping(enableSmileys: false); + /// + /// The default emoji shortcodes mapping, without smileys. + /// + public static readonly EmojiMapping DefaultEmojisOnlyMapping = new EmojiMapping(enableSmileys: false); - internal CompactPrefixTree PrefixTree { get; } + internal CompactPrefixTree PrefixTree { get; } - internal char[] OpeningCharacters { get; } + internal char[] OpeningCharacters { get; } - #region Emojis and Smileys + #region Emojis and Smileys - /// - /// Returns a new instance of the default emoji shortcode to emoji unicode dictionary. - /// It can be used to create a customized . - /// - public static IDictionary GetDefaultEmojiShortcodeToUnicode() + /// + /// Returns a new instance of the default emoji shortcode to emoji unicode dictionary. + /// It can be used to create a customized . + /// + public static IDictionary GetDefaultEmojiShortcodeToUnicode() + { + return new Dictionary(1603) { - return new Dictionary(1603) - { - {":100:", "💯"}, - {":1234:", "🔢"}, - {":smile:", "😄"}, - {":smiley:", "😃"}, - {":grinning:", "😀"}, - {":blush:", "😊"}, - {":relaxed:", "☺️"}, - {":wink:", "😉"}, - {":heart_eyes:", "😍"}, - {":kissing_heart:", "😘"}, - {":kissing_closed_eyes:", "😚"}, - {":kissing:", "😗"}, - {":kissing_smiling_eyes:", "😙"}, - {":stuck_out_tongue_winking_eye:", "😜"}, - {":stuck_out_tongue_closed_eyes:", "😝"}, - {":stuck_out_tongue:", "😛"}, - {":flushed:", "😳"}, - {":grin:", "😁"}, - {":pensive:", "😔"}, - {":relieved:", "😌"}, - {":unamused:", "😒"}, - {":disappointed:", "😞"}, - {":persevere:", "😣"}, - {":cry:", "😢"}, - {":joy:", "😂"}, - {":sob:", "😭"}, - {":sleepy:", "😪"}, - {":disappointed_relieved:", "😥"}, - {":cold_sweat:", "😰"}, - {":sweat_smile:", "😅"}, - {":sweat:", "😓"}, - {":weary:", "😩"}, - {":tired_face:", "😫"}, - {":fearful:", "😨"}, - {":scream:", "😱"}, - {":angry:", "😠"}, - {":rage:", "😡"}, - {":triumph:", "😤"}, - {":confounded:", "😖"}, - {":laughing:", "😆"}, - {":satisfied:", "😆"}, - {":yum:", "😋"}, - {":mask:", "😷"}, - {":sunglasses:", "😎"}, - {":sleeping:", "😴"}, - {":dizzy_face:", "😵"}, - {":astonished:", "😲"}, - {":worried:", "😟"}, - {":frowning:", "😦"}, - {":anguished:", "😧"}, - {":smiling_imp:", "😈"}, - {":imp:", "👿"}, - {":open_mouth:", "😮"}, - {":grimacing:", "😬"}, - {":neutral_face:", "😐"}, - {":confused:", "😕"}, - {":hushed:", "😯"}, - {":no_mouth:", "😶"}, - {":innocent:", "😇"}, - {":smirk:", "😏"}, - {":expressionless:", "😑"}, - {":man_with_gua_pi_mao:", "👲"}, - {":man_with_turban:", "👳"}, - {":cop:", "👮"}, - {":construction_worker:", "👷"}, - {":guardsman:", "💂"}, - {":baby:", "👶"}, - {":boy:", "👦"}, - {":girl:", "👧"}, - {":man:", "👨"}, - {":woman:", "👩"}, - {":older_man:", "👴"}, - {":older_woman:", "👵"}, - {":person_with_blond_hair:", "👱"}, - {":angel:", "👼"}, - {":princess:", "👸"}, - {":smiley_cat:", "😺"}, - {":smile_cat:", "😸"}, - {":heart_eyes_cat:", "😻"}, - {":kissing_cat:", "😽"}, - {":smirk_cat:", "😼"}, - {":scream_cat:", "🙀"}, - {":crying_cat_face:", "😿"}, - {":joy_cat:", "😹"}, - {":pouting_cat:", "😾"}, - {":japanese_ogre:", "👹"}, - {":japanese_goblin:", "👺"}, - {":see_no_evil:", "🙈"}, - {":hear_no_evil:", "🙉"}, - {":speak_no_evil:", "🙊"}, - {":skull:", "💀"}, - {":alien:", "👽"}, - {":hankey:", "💩"}, - {":poop:", "💩"}, - {":shit:", "💩"}, - {":fire:", "🔥"}, - {":sparkles:", "✨"}, - {":star2:", "🌟"}, - {":dizzy:", "💫"}, - {":boom:", "💥"}, - {":collision:", "💥"}, - {":anger:", "💢"}, - {":sweat_drops:", "💦"}, - {":droplet:", "💧"}, - {":zzz:", "💤"}, - {":dash:", "💨"}, - {":ear:", "👂"}, - {":eyes:", "👀"}, - {":nose:", "👃"}, - {":tongue:", "👅"}, - {":lips:", "👄"}, - {":+1:", "👍"}, - {":thumbsup:", "👍"}, - {":-1:", "👎"}, - {":thumbsdown:", "👎"}, - {":ok_hand:", "👌"}, - {":facepunch:", "👊"}, - {":punch:", "👊"}, - {":fist:", "✊"}, - {":v:", "✌️"}, - {":wave:", "👋"}, - {":hand:", "✋"}, - {":raised_hand:", "✋"}, - {":open_hands:", "👐"}, - {":point_up_2:", "👆"}, - {":point_down:", "👇"}, - {":point_right:", "👉"}, - {":point_left:", "👈"}, - {":raised_hands:", "🙌"}, - {":pray:", "🙏"}, - {":point_up:", "☝️"}, - {":clap:", "👏"}, - {":muscle:", "💪"}, - {":walking:", "🚶"}, - {":runner:", "🏃"}, - {":running:", "🏃"}, - {":dancer:", "💃"}, - {":couple:", "👫"}, - {":family:", "👪"}, - {":two_men_holding_hands:", "👬"}, - {":two_women_holding_hands:", "👭"}, - {":couplekiss:", "💏"}, - {":couple_with_heart:", "💑"}, - {":dancers:", "👯"}, - {":ok_woman:", "🙆"}, - {":no_good:", "🙅"}, - {":information_desk_person:", "💁"}, - {":raising_hand:", "🙋"}, - {":massage:", "💆"}, - {":haircut:", "💇"}, - {":nail_care:", "💅"}, - {":bride_with_veil:", "👰"}, - {":person_with_pouting_face:", "🙎"}, - {":person_frowning:", "🙍"}, - {":bow:", "🙇"}, - {":tophat:", "🎩"}, - {":crown:", "👑"}, - {":womans_hat:", "👒"}, - {":athletic_shoe:", "👟"}, - {":mans_shoe:", "👞"}, - {":shoe:", "👞"}, - {":sandal:", "👡"}, - {":high_heel:", "👠"}, - {":boot:", "👢"}, - {":shirt:", "👕"}, - {":tshirt:", "👕"}, - {":necktie:", "👔"}, - {":womans_clothes:", "👚"}, - {":dress:", "👗"}, - {":running_shirt_with_sash:", "🎽"}, - {":jeans:", "👖"}, - {":kimono:", "👘"}, - {":bikini:", "👙"}, - {":briefcase:", "💼"}, - {":handbag:", "👜"}, - {":pouch:", "👝"}, - {":purse:", "👛"}, - {":eyeglasses:", "👓"}, - {":ribbon:", "🎀"}, - {":closed_umbrella:", "🌂"}, - {":lipstick:", "💄"}, - {":yellow_heart:", "💛"}, - {":blue_heart:", "💙"}, - {":purple_heart:", "💜"}, - {":green_heart:", "💚"}, - {":heart:", "❤️"}, - {":broken_heart:", "💔"}, - {":heartpulse:", "💗"}, - {":heartbeat:", "💓"}, - {":two_hearts:", "💕"}, - {":sparkling_heart:", "💖"}, - {":revolving_hearts:", "💞"}, - {":cupid:", "💘"}, - {":love_letter:", "💌"}, - {":kiss:", "💋"}, - {":ring:", "💍"}, - {":gem:", "💎"}, - {":bust_in_silhouette:", "👤"}, - {":busts_in_silhouette:", "👥"}, - {":speech_balloon:", "💬"}, - {":footprints:", "👣"}, - {":thought_balloon:", "💭"}, - {":dog:", "🐶"}, - {":wolf:", "🐺"}, - {":cat:", "🐱"}, - {":mouse:", "🐭"}, - {":hamster:", "🐹"}, - {":rabbit:", "🐰"}, - {":frog:", "🐸"}, - {":tiger:", "🐯"}, - {":koala:", "🐨"}, - {":bear:", "🐻"}, - {":pig:", "🐷"}, - {":pig_nose:", "🐽"}, - {":cow:", "🐮"}, - {":boar:", "🐗"}, - {":monkey_face:", "🐵"}, - {":monkey:", "🐒"}, - {":horse:", "🐴"}, - {":sheep:", "🐑"}, - {":elephant:", "🐘"}, - {":panda_face:", "🐼"}, - {":penguin:", "🐧"}, - {":bird:", "🐦"}, - {":baby_chick:", "🐤"}, - {":hatched_chick:", "🐥"}, - {":hatching_chick:", "🐣"}, - {":chicken:", "🐔"}, - {":snake:", "🐍"}, - {":turtle:", "🐢"}, - {":bug:", "🐛"}, - {":bee:", "🐝"}, - {":honeybee:", "🐝"}, - {":ant:", "🐜"}, - {":beetle:", "🐞"}, - {":snail:", "🐌"}, - {":octopus:", "🐙"}, - {":shell:", "🐚"}, - {":tropical_fish:", "🐠"}, - {":fish:", "🐟"}, - {":dolphin:", "🐬"}, - {":flipper:", "🐬"}, - {":whale:", "🐳"}, - {":whale2:", "🐋"}, - {":cow2:", "🐄"}, - {":ram:", "🐏"}, - {":rat:", "🐀"}, - {":water_buffalo:", "🐃"}, - {":tiger2:", "🐅"}, - {":rabbit2:", "🐇"}, - {":dragon:", "🐉"}, - {":racehorse:", "🐎"}, - {":goat:", "🐐"}, - {":rooster:", "🐓"}, - {":dog2:", "🐕"}, - {":pig2:", "🐖"}, - {":mouse2:", "🐁"}, - {":ox:", "🐂"}, - {":dragon_face:", "🐲"}, - {":blowfish:", "🐡"}, - {":crocodile:", "🐊"}, - {":camel:", "🐫"}, - {":dromedary_camel:", "🐪"}, - {":leopard:", "🐆"}, - {":cat2:", "🐈"}, - {":poodle:", "🐩"}, - {":feet:", "🐾"}, - {":paw_prints:", "🐾"}, - {":bouquet:", "💐"}, - {":cherry_blossom:", "🌸"}, - {":tulip:", "🌷"}, - {":four_leaf_clover:", "🍀"}, - {":rose:", "🌹"}, - {":sunflower:", "🌻"}, - {":hibiscus:", "🌺"}, - {":maple_leaf:", "🍁"}, - {":leaves:", "🍃"}, - {":fallen_leaf:", "🍂"}, - {":herb:", "🌿"}, - {":ear_of_rice:", "🌾"}, - {":mushroom:", "🍄"}, - {":cactus:", "🌵"}, - {":palm_tree:", "🌴"}, - {":evergreen_tree:", "🌲"}, - {":deciduous_tree:", "🌳"}, - {":chestnut:", "🌰"}, - {":seedling:", "🌱"}, - {":blossom:", "🌼"}, - {":globe_with_meridians:", "🌐"}, - {":sun_with_face:", "🌞"}, - {":full_moon_with_face:", "🌝"}, - {":new_moon_with_face:", "🌚"}, - {":new_moon:", "🌑"}, - {":waxing_crescent_moon:", "🌒"}, - {":first_quarter_moon:", "🌓"}, - {":moon:", "🌔"}, - {":waxing_gibbous_moon:", "🌔"}, - {":full_moon:", "🌕"}, - {":waning_gibbous_moon:", "🌖"}, - {":last_quarter_moon:", "🌗"}, - {":waning_crescent_moon:", "🌘"}, - {":last_quarter_moon_with_face:", "🌜"}, - {":first_quarter_moon_with_face:", "🌛"}, - {":crescent_moon:", "🌙"}, - {":earth_africa:", "🌍"}, - {":earth_americas:", "🌎"}, - {":earth_asia:", "🌏"}, - {":volcano:", "🌋"}, - {":milky_way:", "🌌"}, - {":stars:", "🌠"}, - {":star:", "⭐"}, - {":sunny:", "☀️"}, - {":partly_sunny:", "⛅"}, - {":cloud:", "☁️"}, - {":zap:", "⚡"}, - {":umbrella:", "☔"}, - {":snowflake:", "❄️"}, - {":snowman:", "⛄"}, - {":cyclone:", "🌀"}, - {":foggy:", "🌁"}, - {":rainbow:", "🌈"}, - {":ocean:", "🌊"}, - {":bamboo:", "🎍"}, - {":gift_heart:", "💝"}, - {":dolls:", "🎎"}, - {":school_satchel:", "🎒"}, - {":mortar_board:", "🎓"}, - {":flags:", "🎏"}, - {":fireworks:", "🎆"}, - {":sparkler:", "🎇"}, - {":wind_chime:", "🎐"}, - {":rice_scene:", "🎑"}, - {":jack_o_lantern:", "🎃"}, - {":ghost:", "👻"}, - {":santa:", "🎅"}, - {":christmas_tree:", "🎄"}, - {":gift:", "🎁"}, - {":tanabata_tree:", "🎋"}, - {":tada:", "🎉"}, - {":confetti_ball:", "🎊"}, - {":balloon:", "🎈"}, - {":crossed_flags:", "🎌"}, - {":crystal_ball:", "🔮"}, - {":movie_camera:", "🎥"}, - {":camera:", "📷"}, - {":video_camera:", "📹"}, - {":vhs:", "📼"}, - {":cd:", "💿"}, - {":dvd:", "📀"}, - {":minidisc:", "💽"}, - {":floppy_disk:", "💾"}, - {":computer:", "💻"}, - {":iphone:", "📱"}, - {":phone:", "☎️"}, - {":telephone:", "☎️"}, - {":telephone_receiver:", "📞"}, - {":pager:", "📟"}, - {":fax:", "📠"}, - {":satellite:", "📡"}, - {":tv:", "📺"}, - {":radio:", "📻"}, - {":loud_sound:", "🔊"}, - {":sound:", "🔉"}, - {":speaker:", "🔈"}, - {":mute:", "🔇"}, - {":bell:", "🔔"}, - {":no_bell:", "🔕"}, - {":loudspeaker:", "📢"}, - {":mega:", "📣"}, - {":hourglass_flowing_sand:", "⏳"}, - {":hourglass:", "⌛"}, - {":alarm_clock:", "⏰"}, - {":watch:", "⌚"}, - {":unlock:", "🔓"}, - {":lock:", "🔒"}, - {":lock_with_ink_pen:", "🔏"}, - {":closed_lock_with_key:", "🔐"}, - {":key:", "🔑"}, - {":mag_right:", "🔎"}, - {":bulb:", "💡"}, - {":flashlight:", "🔦"}, - {":high_brightness:", "🔆"}, - {":low_brightness:", "🔅"}, - {":electric_plug:", "🔌"}, - {":battery:", "🔋"}, - {":mag:", "🔍"}, - {":bathtub:", "🛁"}, - {":bath:", "🛀"}, - {":shower:", "🚿"}, - {":toilet:", "🚽"}, - {":wrench:", "🔧"}, - {":nut_and_bolt:", "🔩"}, - {":hammer:", "🔨"}, - {":door:", "🚪"}, - {":smoking:", "🚬"}, - {":bomb:", "💣"}, - {":gun:", "🔫"}, - {":hocho:", "🔪"}, - {":knife:", "🔪"}, - {":pill:", "💊"}, - {":syringe:", "💉"}, - {":moneybag:", "💰"}, - {":yen:", "💴"}, - {":dollar:", "💵"}, - {":pound:", "💷"}, - {":euro:", "💶"}, - {":credit_card:", "💳"}, - {":money_with_wings:", "💸"}, - {":calling:", "📲"}, - {":e-mail:", "📧"}, - {":inbox_tray:", "📥"}, - {":outbox_tray:", "📤"}, - {":email:", "✉️"}, - {":envelope:", "✉️"}, - {":envelope_with_arrow:", "📩"}, - {":incoming_envelope:", "📨"}, - {":postal_horn:", "📯"}, - {":mailbox:", "📫"}, - {":mailbox_closed:", "📪"}, - {":mailbox_with_mail:", "📬"}, - {":mailbox_with_no_mail:", "📭"}, - {":postbox:", "📮"}, - {":package:", "📦"}, - {":memo:", "📝"}, - {":pencil:", "📝"}, - {":page_facing_up:", "📄"}, - {":page_with_curl:", "📃"}, - {":bookmark_tabs:", "📑"}, - {":bar_chart:", "📊"}, - {":chart_with_upwards_trend:", "📈"}, - {":chart_with_downwards_trend:", "📉"}, - {":scroll:", "📜"}, - {":clipboard:", "📋"}, - {":date:", "📅"}, - {":calendar:", "📆"}, - {":card_index:", "📇"}, - {":file_folder:", "📁"}, - {":open_file_folder:", "📂"}, - {":scissors:", "✂️"}, - {":pushpin:", "📌"}, - {":paperclip:", "📎"}, - {":black_nib:", "✒️"}, - {":pencil2:", "✏️"}, - {":straight_ruler:", "📏"}, - {":triangular_ruler:", "📐"}, - {":closed_book:", "📕"}, - {":green_book:", "📗"}, - {":blue_book:", "📘"}, - {":orange_book:", "📙"}, - {":notebook:", "📓"}, - {":notebook_with_decorative_cover:", "📔"}, - {":ledger:", "📒"}, - {":books:", "📚"}, - {":book:", "📖"}, - {":open_book:", "📖"}, - {":bookmark:", "🔖"}, - {":name_badge:", "📛"}, - {":microscope:", "🔬"}, - {":telescope:", "🔭"}, - {":newspaper:", "📰"}, - {":art:", "🎨"}, - {":clapper:", "🎬"}, - {":microphone:", "🎤"}, - {":headphones:", "🎧"}, - {":musical_score:", "🎼"}, - {":musical_note:", "🎵"}, - {":notes:", "🎶"}, - {":musical_keyboard:", "🎹"}, - {":violin:", "🎻"}, - {":trumpet:", "🎺"}, - {":saxophone:", "🎷"}, - {":guitar:", "🎸"}, - {":space_invader:", "👾"}, - {":video_game:", "🎮"}, - {":black_joker:", "🃏"}, - {":flower_playing_cards:", "🎴"}, - {":mahjong:", "🀄"}, - {":game_die:", "🎲"}, - {":dart:", "🎯"}, - {":football:", "🏈"}, - {":basketball:", "🏀"}, - {":soccer:", "⚽"}, - {":baseball:", "⚾️"}, - {":tennis:", "🎾"}, - {":8ball:", "🎱"}, - {":rugby_football:", "🏉"}, - {":bowling:", "🎳"}, - {":golf:", "⛳"}, - {":mountain_bicyclist:", "🚵"}, - {":bicyclist:", "🚴"}, - {":checkered_flag:", "🏁"}, - {":horse_racing:", "🏇"}, - {":trophy:", "🏆"}, - {":ski:", "🎿"}, - {":snowboarder:", "🏂"}, - {":swimmer:", "🏊"}, - {":surfer:", "🏄"}, - {":fishing_pole_and_fish:", "🎣"}, - {":coffee:", "☕"}, - {":tea:", "🍵"}, - {":sake:", "🍶"}, - {":baby_bottle:", "🍼"}, - {":beer:", "🍺"}, - {":beers:", "🍻"}, - {":cocktail:", "🍸"}, - {":tropical_drink:", "🍹"}, - {":wine_glass:", "🍷"}, - {":fork_and_knife:", "🍴"}, - {":pizza:", "🍕"}, - {":hamburger:", "🍔"}, - {":fries:", "🍟"}, - {":poultry_leg:", "🍗"}, - {":meat_on_bone:", "🍖"}, - {":spaghetti:", "🍝"}, - {":curry:", "🍛"}, - {":fried_shrimp:", "🍤"}, - {":bento:", "🍱"}, - {":sushi:", "🍣"}, - {":fish_cake:", "🍥"}, - {":rice_ball:", "🍙"}, - {":rice_cracker:", "🍘"}, - {":rice:", "🍚"}, - {":ramen:", "🍜"}, - {":stew:", "🍲"}, - {":oden:", "🍢"}, - {":dango:", "🍡"}, - {":egg:", "🍳"}, - {":bread:", "🍞"}, - {":doughnut:", "🍩"}, - {":custard:", "🍮"}, - {":icecream:", "🍦"}, - {":ice_cream:", "🍨"}, - {":shaved_ice:", "🍧"}, - {":birthday:", "🎂"}, - {":cake:", "🍰"}, - {":cookie:", "🍪"}, - {":chocolate_bar:", "🍫"}, - {":candy:", "🍬"}, - {":lollipop:", "🍭"}, - {":honey_pot:", "🍯"}, - {":apple:", "🍎"}, - {":green_apple:", "🍏"}, - {":tangerine:", "🍊"}, - {":lemon:", "🍋"}, - {":cherries:", "🍒"}, - {":grapes:", "🍇"}, - {":watermelon:", "🍉"}, - {":strawberry:", "🍓"}, - {":peach:", "🍑"}, - {":melon:", "🍈"}, - {":banana:", "🍌"}, - {":pear:", "🍐"}, - {":pineapple:", "🍍"}, - {":sweet_potato:", "🍠"}, - {":eggplant:", "🍆"}, - {":tomato:", "🍅"}, - {":corn:", "🌽"}, - {":house:", "🏠"}, - {":house_with_garden:", "🏡"}, - {":school:", "🏫"}, - {":office:", "🏢"}, - {":post_office:", "🏣"}, - {":hospital:", "🏥"}, - {":bank:", "🏦"}, - {":convenience_store:", "🏪"}, - {":love_hotel:", "🏩"}, - {":hotel:", "🏨"}, - {":wedding:", "💒"}, - {":church:", "⛪"}, - {":department_store:", "🏬"}, - {":european_post_office:", "🏤"}, - {":city_sunrise:", "🌇"}, - {":city_sunset:", "🌆"}, - {":japanese_castle:", "🏯"}, - {":european_castle:", "🏰"}, - {":tent:", "⛺"}, - {":factory:", "🏭"}, - {":tokyo_tower:", "🗼"}, - {":japan:", "🗾"}, - {":mount_fuji:", "🗻"}, - {":sunrise_over_mountains:", "🌄"}, - {":sunrise:", "🌅"}, - {":night_with_stars:", "🌃"}, - {":statue_of_liberty:", "🗽"}, - {":bridge_at_night:", "🌉"}, - {":carousel_horse:", "🎠"}, - {":ferris_wheel:", "🎡"}, - {":fountain:", "⛲"}, - {":roller_coaster:", "🎢"}, - {":ship:", "🚢"}, - {":boat:", "⛵"}, - {":sailboat:", "⛵"}, - {":speedboat:", "🚤"}, - {":rowboat:", "🚣"}, - {":anchor:", "⚓"}, - {":rocket:", "🚀"}, - {":airplane:", "✈️"}, - {":seat:", "💺"}, - {":helicopter:", "🚁"}, - {":steam_locomotive:", "🚂"}, - {":tram:", "🚊"}, - {":station:", "🚉"}, - {":mountain_railway:", "🚞"}, - {":train2:", "🚆"}, - {":bullettrain_side:", "🚄"}, - {":bullettrain_front:", "🚅"}, - {":light_rail:", "🚈"}, - {":metro:", "🚇"}, - {":monorail:", "🚝"}, - {":train:", "🚋"}, - {":railway_car:", "🚃"}, - {":trolleybus:", "🚎"}, - {":bus:", "🚌"}, - {":oncoming_bus:", "🚍"}, - {":blue_car:", "🚙"}, - {":oncoming_automobile:", "🚘"}, - {":car:", "🚗"}, - {":red_car:", "🚗"}, - {":taxi:", "🚕"}, - {":oncoming_taxi:", "🚖"}, - {":articulated_lorry:", "🚛"}, - {":truck:", "🚚"}, - {":rotating_light:", "🚨"}, - {":police_car:", "🚓"}, - {":oncoming_police_car:", "🚔"}, - {":fire_engine:", "🚒"}, - {":ambulance:", "🚑"}, - {":minibus:", "🚐"}, - {":bike:", "🚲"}, - {":aerial_tramway:", "🚡"}, - {":suspension_railway:", "🚟"}, - {":mountain_cableway:", "🚠"}, - {":tractor:", "🚜"}, - {":barber:", "💈"}, - {":busstop:", "🚏"}, - {":ticket:", "🎫"}, - {":vertical_traffic_light:", "🚦"}, - {":traffic_light:", "🚥"}, - {":warning:", "⚠️"}, - {":construction:", "🚧"}, - {":beginner:", "🔰"}, - {":fuelpump:", "⛽"}, - {":izakaya_lantern:", "🏮"}, - {":lantern:", "🏮"}, - {":slot_machine:", "🎰"}, - {":hotsprings:", "♨️"}, - {":moyai:", "🗿"}, - {":circus_tent:", "🎪"}, - {":performing_arts:", "🎭"}, - {":round_pushpin:", "📍"}, - {":triangular_flag_on_post:", "🚩"}, - {":jp:", "🇯🇵"}, - {":kr:", "🇰🇷"}, - {":de:", "🇩🇪"}, - {":cn:", "🇨🇳"}, - {":us:", "🇺🇸"}, - {":fr:", "🇫🇷"}, - {":es:", "🇪🇸"}, - {":it:", "🇮🇹"}, - {":ru:", "🇷🇺"}, - {":gb:", "🇬🇧"}, - {":uk:", "🇬🇧"}, - {":one:", "1️⃣"}, - {":two:", "2️⃣"}, - {":three:", "3️⃣"}, - {":four:", "4️⃣"}, - {":five:", "5️⃣"}, - {":six:", "6️⃣"}, - {":seven:", "7️⃣"}, - {":eight:", "8️⃣"}, - {":nine:", "9️⃣"}, - {":zero:", "0️⃣"}, - {":keycap_ten:", "🔟"}, - {":hash:", "#️⃣"}, - {":symbols:", "🔣"}, - {":arrow_up:", "⬆️"}, - {":arrow_down:", "⬇️"}, - {":arrow_left:", "⬅️"}, - {":arrow_right:", "➡️"}, - {":capital_abcd:", "🔠"}, - {":abcd:", "🔡"}, - {":abc:", "🔤"}, - {":arrow_upper_right:", "↗️"}, - {":arrow_upper_left:", "↖️"}, - {":arrow_lower_right:", "↘️"}, - {":arrow_lower_left:", "↙️"}, - {":left_right_arrow:", "↔️"}, - {":arrow_up_down:", "↕️"}, - {":arrows_counterclockwise:", "🔄"}, - {":arrow_backward:", "◀️"}, - {":arrow_forward:", "▶️"}, - {":arrow_up_small:", "🔼"}, - {":arrow_down_small:", "🔽"}, - {":leftwards_arrow_with_hook:", "↩️"}, - {":arrow_right_hook:", "↪️"}, - {":information_source:", "ℹ️"}, - {":rewind:", "⏪"}, - {":fast_forward:", "⏩"}, - {":arrow_double_up:", "⏫"}, - {":arrow_double_down:", "⏬"}, - {":arrow_heading_down:", "⤵️"}, - {":arrow_heading_up:", "⤴️"}, - {":ok:", "🆗"}, - {":twisted_rightwards_arrows:", "🔀"}, - {":repeat:", "🔁"}, - {":repeat_one:", "🔂"}, - {":new:", "🆕"}, - {":up:", "🆙"}, - {":cool:", "🆒"}, - {":free:", "🆓"}, - {":ng:", "🆖"}, - {":signal_strength:", "📶"}, - {":cinema:", "🎦"}, - {":koko:", "🈁"}, - {":u6307:", "🈯"}, - {":u7a7a:", "🈳"}, - {":u6e80:", "🈵"}, - {":u5408:", "🈴"}, - {":u7981:", "🈲"}, - {":ideograph_advantage:", "🉐"}, - {":u5272:", "🈹"}, - {":u55b6:", "🈺"}, - {":u6709:", "🈶"}, - {":u7121:", "🈚"}, - {":restroom:", "🚻"}, - {":mens:", "🚹"}, - {":womens:", "🚺"}, - {":baby_symbol:", "🚼"}, - {":wc:", "🚾"}, - {":potable_water:", "🚰"}, - {":put_litter_in_its_place:", "🚮"}, - {":parking:", "🅿️"}, - {":wheelchair:", "♿"}, - {":no_smoking:", "🚭"}, - {":u6708:", "🈷️"}, - {":u7533:", "🈸"}, - {":sa:", "🈂️"}, - {":m:", "Ⓜ️"}, - {":passport_control:", "🛂"}, - {":baggage_claim:", "🛄"}, - {":left_luggage:", "🛅"}, - {":customs:", "🛃"}, - {":accept:", "🉑"}, - {":secret:", "㊙️"}, - {":congratulations:", "㊗️"}, - {":cl:", "🆑"}, - {":sos:", "🆘"}, - {":id:", "🆔"}, - {":no_entry_sign:", "🚫"}, - {":underage:", "🔞"}, - {":no_mobile_phones:", "📵"}, - {":do_not_litter:", "🚯"}, - {":non-potable_water:", "🚱"}, - {":no_bicycles:", "🚳"}, - {":no_pedestrians:", "🚷"}, - {":children_crossing:", "🚸"}, - {":no_entry:", "⛔"}, - {":eight_spoked_asterisk:", "✳️"}, - {":sparkle:", "❇️"}, - {":negative_squared_cross_mark:", "❎"}, - {":white_check_mark:", "✅"}, - {":eight_pointed_black_star:", "✴️"}, - {":heart_decoration:", "💟"}, - {":vs:", "🆚"}, - {":vibration_mode:", "📳"}, - {":mobile_phone_off:", "📴"}, - {":a:", "🅰️"}, - {":b:", "🅱️"}, - {":ab:", "🆎"}, - {":o2:", "🅾️"}, - {":diamond_shape_with_a_dot_inside:", "💠"}, - {":loop:", "➿"}, - {":recycle:", "♻️"}, - {":aries:", "♈"}, - {":taurus:", "♉"}, - {":gemini:", "♊"}, - {":cancer:", "♋"}, - {":leo:", "♌"}, - {":virgo:", "♍"}, - {":libra:", "♎"}, - {":scorpius:", "♏"}, - {":sagittarius:", "♐"}, - {":capricorn:", "♑"}, - {":aquarius:", "♒"}, - {":pisces:", "♓"}, - {":ophiuchus:", "⛎"}, - {":six_pointed_star:", "🔯"}, - {":atm:", "🏧"}, - {":chart:", "💹"}, - {":heavy_dollar_sign:", "💲"}, - {":currency_exchange:", "💱"}, - {":copyright:", "©️"}, - {":registered:", "®️"}, - {":tm:", "™️"}, - {":x:", "❌"}, - {":bangbang:", "‼️"}, - {":interrobang:", "⁉️"}, - {":exclamation:", "❗"}, - {":heavy_exclamation_mark:", "❗"}, - {":question:", "❓"}, - {":grey_exclamation:", "❕"}, - {":grey_question:", "❔"}, - {":o:", "⭕"}, - {":top:", "🔝"}, - {":end:", "🔚"}, - {":back:", "🔙"}, - {":on:", "🔛"}, - {":soon:", "🔜"}, - {":arrows_clockwise:", "🔃"}, - {":clock12:", "🕛"}, - {":clock1230:", "🕧"}, - {":clock1:", "🕐"}, - {":clock130:", "🕜"}, - {":clock2:", "🕑"}, - {":clock230:", "🕝"}, - {":clock3:", "🕒"}, - {":clock330:", "🕞"}, - {":clock4:", "🕓"}, - {":clock430:", "🕟"}, - {":clock5:", "🕔"}, - {":clock530:", "🕠"}, - {":clock6:", "🕕"}, - {":clock7:", "🕖"}, - {":clock8:", "🕗"}, - {":clock9:", "🕘"}, - {":clock10:", "🕙"}, - {":clock11:", "🕚"}, - {":clock630:", "🕡"}, - {":clock730:", "🕢"}, - {":clock830:", "🕣"}, - {":clock930:", "🕤"}, - {":clock1030:", "🕥"}, - {":clock1130:", "🕦"}, - {":heavy_multiplication_x:", "✖️"}, - {":heavy_plus_sign:", "➕"}, - {":heavy_minus_sign:", "➖"}, - {":heavy_division_sign:", "➗"}, - {":spades:", "♠️"}, - {":hearts:", "♥️"}, - {":clubs:", "♣️"}, - {":diamonds:", "♦️"}, - {":white_flower:", "💮"}, - {":heavy_check_mark:", "✔️"}, - {":ballot_box_with_check:", "☑️"}, - {":radio_button:", "🔘"}, - {":link:", "🔗"}, - {":curly_loop:", "➰"}, - {":wavy_dash:", "〰️"}, - {":part_alternation_mark:", "〽️"}, - {":trident:", "🔱"}, - {":black_medium_square:", "◼️"}, - {":white_medium_square:", "◻️"}, - {":black_medium_small_square:", "◾"}, - {":white_medium_small_square:", "◽"}, - {":black_small_square:", "▪️"}, - {":white_small_square:", "▫️"}, - {":small_red_triangle:", "🔺"}, - {":black_square_button:", "🔲"}, - {":white_square_button:", "🔳"}, - {":black_circle:", "⚫"}, - {":white_circle:", "⚪"}, - {":red_circle:", "🔴"}, - {":large_blue_circle:", "🔵"}, - {":small_red_triangle_down:", "🔻"}, - {":white_large_square:", "⬜"}, - {":black_large_square:", "⬛"}, - {":large_orange_diamond:", "🔶"}, - {":large_blue_diamond:", "🔷"}, - {":small_orange_diamond:", "🔸"}, - {":small_blue_diamond:", "🔹"}, + {":100:", "💯"}, + {":1234:", "🔢"}, + {":smile:", "😄"}, + {":smiley:", "😃"}, + {":grinning:", "😀"}, + {":blush:", "😊"}, + {":relaxed:", "☺️"}, + {":wink:", "😉"}, + {":heart_eyes:", "😍"}, + {":kissing_heart:", "😘"}, + {":kissing_closed_eyes:", "😚"}, + {":kissing:", "😗"}, + {":kissing_smiling_eyes:", "😙"}, + {":stuck_out_tongue_winking_eye:", "😜"}, + {":stuck_out_tongue_closed_eyes:", "😝"}, + {":stuck_out_tongue:", "😛"}, + {":flushed:", "😳"}, + {":grin:", "😁"}, + {":pensive:", "😔"}, + {":relieved:", "😌"}, + {":unamused:", "😒"}, + {":disappointed:", "😞"}, + {":persevere:", "😣"}, + {":cry:", "😢"}, + {":joy:", "😂"}, + {":sob:", "😭"}, + {":sleepy:", "😪"}, + {":disappointed_relieved:", "😥"}, + {":cold_sweat:", "😰"}, + {":sweat_smile:", "😅"}, + {":sweat:", "😓"}, + {":weary:", "😩"}, + {":tired_face:", "😫"}, + {":fearful:", "😨"}, + {":scream:", "😱"}, + {":angry:", "😠"}, + {":rage:", "😡"}, + {":triumph:", "😤"}, + {":confounded:", "😖"}, + {":laughing:", "😆"}, + {":satisfied:", "😆"}, + {":yum:", "😋"}, + {":mask:", "😷"}, + {":sunglasses:", "😎"}, + {":sleeping:", "😴"}, + {":dizzy_face:", "😵"}, + {":astonished:", "😲"}, + {":worried:", "😟"}, + {":frowning:", "😦"}, + {":anguished:", "😧"}, + {":smiling_imp:", "😈"}, + {":imp:", "👿"}, + {":open_mouth:", "😮"}, + {":grimacing:", "😬"}, + {":neutral_face:", "😐"}, + {":confused:", "😕"}, + {":hushed:", "😯"}, + {":no_mouth:", "😶"}, + {":innocent:", "😇"}, + {":smirk:", "😏"}, + {":expressionless:", "😑"}, + {":man_with_gua_pi_mao:", "👲"}, + {":man_with_turban:", "👳"}, + {":cop:", "👮"}, + {":construction_worker:", "👷"}, + {":guardsman:", "💂"}, + {":baby:", "👶"}, + {":boy:", "👦"}, + {":girl:", "👧"}, + {":man:", "👨"}, + {":woman:", "👩"}, + {":older_man:", "👴"}, + {":older_woman:", "👵"}, + {":person_with_blond_hair:", "👱"}, + {":angel:", "👼"}, + {":princess:", "👸"}, + {":smiley_cat:", "😺"}, + {":smile_cat:", "😸"}, + {":heart_eyes_cat:", "😻"}, + {":kissing_cat:", "😽"}, + {":smirk_cat:", "😼"}, + {":scream_cat:", "🙀"}, + {":crying_cat_face:", "😿"}, + {":joy_cat:", "😹"}, + {":pouting_cat:", "😾"}, + {":japanese_ogre:", "👹"}, + {":japanese_goblin:", "👺"}, + {":see_no_evil:", "🙈"}, + {":hear_no_evil:", "🙉"}, + {":speak_no_evil:", "🙊"}, + {":skull:", "💀"}, + {":alien:", "👽"}, + {":hankey:", "💩"}, + {":poop:", "💩"}, + {":shit:", "💩"}, + {":fire:", "🔥"}, + {":sparkles:", "✨"}, + {":star2:", "🌟"}, + {":dizzy:", "💫"}, + {":boom:", "💥"}, + {":collision:", "💥"}, + {":anger:", "💢"}, + {":sweat_drops:", "💦"}, + {":droplet:", "💧"}, + {":zzz:", "💤"}, + {":dash:", "💨"}, + {":ear:", "👂"}, + {":eyes:", "👀"}, + {":nose:", "👃"}, + {":tongue:", "👅"}, + {":lips:", "👄"}, + {":+1:", "👍"}, + {":thumbsup:", "👍"}, + {":-1:", "👎"}, + {":thumbsdown:", "👎"}, + {":ok_hand:", "👌"}, + {":facepunch:", "👊"}, + {":punch:", "👊"}, + {":fist:", "✊"}, + {":v:", "✌️"}, + {":wave:", "👋"}, + {":hand:", "✋"}, + {":raised_hand:", "✋"}, + {":open_hands:", "👐"}, + {":point_up_2:", "👆"}, + {":point_down:", "👇"}, + {":point_right:", "👉"}, + {":point_left:", "👈"}, + {":raised_hands:", "🙌"}, + {":pray:", "🙏"}, + {":point_up:", "☝️"}, + {":clap:", "👏"}, + {":muscle:", "💪"}, + {":walking:", "🚶"}, + {":runner:", "🏃"}, + {":running:", "🏃"}, + {":dancer:", "💃"}, + {":couple:", "👫"}, + {":family:", "👪"}, + {":two_men_holding_hands:", "👬"}, + {":two_women_holding_hands:", "👭"}, + {":couplekiss:", "💏"}, + {":couple_with_heart:", "💑"}, + {":dancers:", "👯"}, + {":ok_woman:", "🙆"}, + {":no_good:", "🙅"}, + {":information_desk_person:", "💁"}, + {":raising_hand:", "🙋"}, + {":massage:", "💆"}, + {":haircut:", "💇"}, + {":nail_care:", "💅"}, + {":bride_with_veil:", "👰"}, + {":person_with_pouting_face:", "🙎"}, + {":person_frowning:", "🙍"}, + {":bow:", "🙇"}, + {":tophat:", "🎩"}, + {":crown:", "👑"}, + {":womans_hat:", "👒"}, + {":athletic_shoe:", "👟"}, + {":mans_shoe:", "👞"}, + {":shoe:", "👞"}, + {":sandal:", "👡"}, + {":high_heel:", "👠"}, + {":boot:", "👢"}, + {":shirt:", "👕"}, + {":tshirt:", "👕"}, + {":necktie:", "👔"}, + {":womans_clothes:", "👚"}, + {":dress:", "👗"}, + {":running_shirt_with_sash:", "🎽"}, + {":jeans:", "👖"}, + {":kimono:", "👘"}, + {":bikini:", "👙"}, + {":briefcase:", "💼"}, + {":handbag:", "👜"}, + {":pouch:", "👝"}, + {":purse:", "👛"}, + {":eyeglasses:", "👓"}, + {":ribbon:", "🎀"}, + {":closed_umbrella:", "🌂"}, + {":lipstick:", "💄"}, + {":yellow_heart:", "💛"}, + {":blue_heart:", "💙"}, + {":purple_heart:", "💜"}, + {":green_heart:", "💚"}, + {":heart:", "❤️"}, + {":broken_heart:", "💔"}, + {":heartpulse:", "💗"}, + {":heartbeat:", "💓"}, + {":two_hearts:", "💕"}, + {":sparkling_heart:", "💖"}, + {":revolving_hearts:", "💞"}, + {":cupid:", "💘"}, + {":love_letter:", "💌"}, + {":kiss:", "💋"}, + {":ring:", "💍"}, + {":gem:", "💎"}, + {":bust_in_silhouette:", "👤"}, + {":busts_in_silhouette:", "👥"}, + {":speech_balloon:", "💬"}, + {":footprints:", "👣"}, + {":thought_balloon:", "💭"}, + {":dog:", "🐶"}, + {":wolf:", "🐺"}, + {":cat:", "🐱"}, + {":mouse:", "🐭"}, + {":hamster:", "🐹"}, + {":rabbit:", "🐰"}, + {":frog:", "🐸"}, + {":tiger:", "🐯"}, + {":koala:", "🐨"}, + {":bear:", "🐻"}, + {":pig:", "🐷"}, + {":pig_nose:", "🐽"}, + {":cow:", "🐮"}, + {":boar:", "🐗"}, + {":monkey_face:", "🐵"}, + {":monkey:", "🐒"}, + {":horse:", "🐴"}, + {":sheep:", "🐑"}, + {":elephant:", "🐘"}, + {":panda_face:", "🐼"}, + {":penguin:", "🐧"}, + {":bird:", "🐦"}, + {":baby_chick:", "🐤"}, + {":hatched_chick:", "🐥"}, + {":hatching_chick:", "🐣"}, + {":chicken:", "🐔"}, + {":snake:", "🐍"}, + {":turtle:", "🐢"}, + {":bug:", "🐛"}, + {":bee:", "🐝"}, + {":honeybee:", "🐝"}, + {":ant:", "🐜"}, + {":beetle:", "🐞"}, + {":snail:", "🐌"}, + {":octopus:", "🐙"}, + {":shell:", "🐚"}, + {":tropical_fish:", "🐠"}, + {":fish:", "🐟"}, + {":dolphin:", "🐬"}, + {":flipper:", "🐬"}, + {":whale:", "🐳"}, + {":whale2:", "🐋"}, + {":cow2:", "🐄"}, + {":ram:", "🐏"}, + {":rat:", "🐀"}, + {":water_buffalo:", "🐃"}, + {":tiger2:", "🐅"}, + {":rabbit2:", "🐇"}, + {":dragon:", "🐉"}, + {":racehorse:", "🐎"}, + {":goat:", "🐐"}, + {":rooster:", "🐓"}, + {":dog2:", "🐕"}, + {":pig2:", "🐖"}, + {":mouse2:", "🐁"}, + {":ox:", "🐂"}, + {":dragon_face:", "🐲"}, + {":blowfish:", "🐡"}, + {":crocodile:", "🐊"}, + {":camel:", "🐫"}, + {":dromedary_camel:", "🐪"}, + {":leopard:", "🐆"}, + {":cat2:", "🐈"}, + {":poodle:", "🐩"}, + {":feet:", "🐾"}, + {":paw_prints:", "🐾"}, + {":bouquet:", "💐"}, + {":cherry_blossom:", "🌸"}, + {":tulip:", "🌷"}, + {":four_leaf_clover:", "🍀"}, + {":rose:", "🌹"}, + {":sunflower:", "🌻"}, + {":hibiscus:", "🌺"}, + {":maple_leaf:", "🍁"}, + {":leaves:", "🍃"}, + {":fallen_leaf:", "🍂"}, + {":herb:", "🌿"}, + {":ear_of_rice:", "🌾"}, + {":mushroom:", "🍄"}, + {":cactus:", "🌵"}, + {":palm_tree:", "🌴"}, + {":evergreen_tree:", "🌲"}, + {":deciduous_tree:", "🌳"}, + {":chestnut:", "🌰"}, + {":seedling:", "🌱"}, + {":blossom:", "🌼"}, + {":globe_with_meridians:", "🌐"}, + {":sun_with_face:", "🌞"}, + {":full_moon_with_face:", "🌝"}, + {":new_moon_with_face:", "🌚"}, + {":new_moon:", "🌑"}, + {":waxing_crescent_moon:", "🌒"}, + {":first_quarter_moon:", "🌓"}, + {":moon:", "🌔"}, + {":waxing_gibbous_moon:", "🌔"}, + {":full_moon:", "🌕"}, + {":waning_gibbous_moon:", "🌖"}, + {":last_quarter_moon:", "🌗"}, + {":waning_crescent_moon:", "🌘"}, + {":last_quarter_moon_with_face:", "🌜"}, + {":first_quarter_moon_with_face:", "🌛"}, + {":crescent_moon:", "🌙"}, + {":earth_africa:", "🌍"}, + {":earth_americas:", "🌎"}, + {":earth_asia:", "🌏"}, + {":volcano:", "🌋"}, + {":milky_way:", "🌌"}, + {":stars:", "🌠"}, + {":star:", "⭐"}, + {":sunny:", "☀️"}, + {":partly_sunny:", "⛅"}, + {":cloud:", "☁️"}, + {":zap:", "⚡"}, + {":umbrella:", "☔"}, + {":snowflake:", "❄️"}, + {":snowman:", "⛄"}, + {":cyclone:", "🌀"}, + {":foggy:", "🌁"}, + {":rainbow:", "🌈"}, + {":ocean:", "🌊"}, + {":bamboo:", "🎍"}, + {":gift_heart:", "💝"}, + {":dolls:", "🎎"}, + {":school_satchel:", "🎒"}, + {":mortar_board:", "🎓"}, + {":flags:", "🎏"}, + {":fireworks:", "🎆"}, + {":sparkler:", "🎇"}, + {":wind_chime:", "🎐"}, + {":rice_scene:", "🎑"}, + {":jack_o_lantern:", "🎃"}, + {":ghost:", "👻"}, + {":santa:", "🎅"}, + {":christmas_tree:", "🎄"}, + {":gift:", "🎁"}, + {":tanabata_tree:", "🎋"}, + {":tada:", "🎉"}, + {":confetti_ball:", "🎊"}, + {":balloon:", "🎈"}, + {":crossed_flags:", "🎌"}, + {":crystal_ball:", "🔮"}, + {":movie_camera:", "🎥"}, + {":camera:", "📷"}, + {":video_camera:", "📹"}, + {":vhs:", "📼"}, + {":cd:", "💿"}, + {":dvd:", "📀"}, + {":minidisc:", "💽"}, + {":floppy_disk:", "💾"}, + {":computer:", "💻"}, + {":iphone:", "📱"}, + {":phone:", "☎️"}, + {":telephone:", "☎️"}, + {":telephone_receiver:", "📞"}, + {":pager:", "📟"}, + {":fax:", "📠"}, + {":satellite:", "📡"}, + {":tv:", "📺"}, + {":radio:", "📻"}, + {":loud_sound:", "🔊"}, + {":sound:", "🔉"}, + {":speaker:", "🔈"}, + {":mute:", "🔇"}, + {":bell:", "🔔"}, + {":no_bell:", "🔕"}, + {":loudspeaker:", "📢"}, + {":mega:", "📣"}, + {":hourglass_flowing_sand:", "⏳"}, + {":hourglass:", "⌛"}, + {":alarm_clock:", "⏰"}, + {":watch:", "⌚"}, + {":unlock:", "🔓"}, + {":lock:", "🔒"}, + {":lock_with_ink_pen:", "🔏"}, + {":closed_lock_with_key:", "🔐"}, + {":key:", "🔑"}, + {":mag_right:", "🔎"}, + {":bulb:", "💡"}, + {":flashlight:", "🔦"}, + {":high_brightness:", "🔆"}, + {":low_brightness:", "🔅"}, + {":electric_plug:", "🔌"}, + {":battery:", "🔋"}, + {":mag:", "🔍"}, + {":bathtub:", "🛁"}, + {":bath:", "🛀"}, + {":shower:", "🚿"}, + {":toilet:", "🚽"}, + {":wrench:", "🔧"}, + {":nut_and_bolt:", "🔩"}, + {":hammer:", "🔨"}, + {":door:", "🚪"}, + {":smoking:", "🚬"}, + {":bomb:", "💣"}, + {":gun:", "🔫"}, + {":hocho:", "🔪"}, + {":knife:", "🔪"}, + {":pill:", "💊"}, + {":syringe:", "💉"}, + {":moneybag:", "💰"}, + {":yen:", "💴"}, + {":dollar:", "💵"}, + {":pound:", "💷"}, + {":euro:", "💶"}, + {":credit_card:", "💳"}, + {":money_with_wings:", "💸"}, + {":calling:", "📲"}, + {":e-mail:", "📧"}, + {":inbox_tray:", "📥"}, + {":outbox_tray:", "📤"}, + {":email:", "✉️"}, + {":envelope:", "✉️"}, + {":envelope_with_arrow:", "📩"}, + {":incoming_envelope:", "📨"}, + {":postal_horn:", "📯"}, + {":mailbox:", "📫"}, + {":mailbox_closed:", "📪"}, + {":mailbox_with_mail:", "📬"}, + {":mailbox_with_no_mail:", "📭"}, + {":postbox:", "📮"}, + {":package:", "📦"}, + {":memo:", "📝"}, + {":pencil:", "📝"}, + {":page_facing_up:", "📄"}, + {":page_with_curl:", "📃"}, + {":bookmark_tabs:", "📑"}, + {":bar_chart:", "📊"}, + {":chart_with_upwards_trend:", "📈"}, + {":chart_with_downwards_trend:", "📉"}, + {":scroll:", "📜"}, + {":clipboard:", "📋"}, + {":date:", "📅"}, + {":calendar:", "📆"}, + {":card_index:", "📇"}, + {":file_folder:", "📁"}, + {":open_file_folder:", "📂"}, + {":scissors:", "✂️"}, + {":pushpin:", "📌"}, + {":paperclip:", "📎"}, + {":black_nib:", "✒️"}, + {":pencil2:", "✏️"}, + {":straight_ruler:", "📏"}, + {":triangular_ruler:", "📐"}, + {":closed_book:", "📕"}, + {":green_book:", "📗"}, + {":blue_book:", "📘"}, + {":orange_book:", "📙"}, + {":notebook:", "📓"}, + {":notebook_with_decorative_cover:", "📔"}, + {":ledger:", "📒"}, + {":books:", "📚"}, + {":book:", "📖"}, + {":open_book:", "📖"}, + {":bookmark:", "🔖"}, + {":name_badge:", "📛"}, + {":microscope:", "🔬"}, + {":telescope:", "🔭"}, + {":newspaper:", "📰"}, + {":art:", "🎨"}, + {":clapper:", "🎬"}, + {":microphone:", "🎤"}, + {":headphones:", "🎧"}, + {":musical_score:", "🎼"}, + {":musical_note:", "🎵"}, + {":notes:", "🎶"}, + {":musical_keyboard:", "🎹"}, + {":violin:", "🎻"}, + {":trumpet:", "🎺"}, + {":saxophone:", "🎷"}, + {":guitar:", "🎸"}, + {":space_invader:", "👾"}, + {":video_game:", "🎮"}, + {":black_joker:", "🃏"}, + {":flower_playing_cards:", "🎴"}, + {":mahjong:", "🀄"}, + {":game_die:", "🎲"}, + {":dart:", "🎯"}, + {":football:", "🏈"}, + {":basketball:", "🏀"}, + {":soccer:", "⚽"}, + {":baseball:", "⚾️"}, + {":tennis:", "🎾"}, + {":8ball:", "🎱"}, + {":rugby_football:", "🏉"}, + {":bowling:", "🎳"}, + {":golf:", "⛳"}, + {":mountain_bicyclist:", "🚵"}, + {":bicyclist:", "🚴"}, + {":checkered_flag:", "🏁"}, + {":horse_racing:", "🏇"}, + {":trophy:", "🏆"}, + {":ski:", "🎿"}, + {":snowboarder:", "🏂"}, + {":swimmer:", "🏊"}, + {":surfer:", "🏄"}, + {":fishing_pole_and_fish:", "🎣"}, + {":coffee:", "☕"}, + {":tea:", "🍵"}, + {":sake:", "🍶"}, + {":baby_bottle:", "🍼"}, + {":beer:", "🍺"}, + {":beers:", "🍻"}, + {":cocktail:", "🍸"}, + {":tropical_drink:", "🍹"}, + {":wine_glass:", "🍷"}, + {":fork_and_knife:", "🍴"}, + {":pizza:", "🍕"}, + {":hamburger:", "🍔"}, + {":fries:", "🍟"}, + {":poultry_leg:", "🍗"}, + {":meat_on_bone:", "🍖"}, + {":spaghetti:", "🍝"}, + {":curry:", "🍛"}, + {":fried_shrimp:", "🍤"}, + {":bento:", "🍱"}, + {":sushi:", "🍣"}, + {":fish_cake:", "🍥"}, + {":rice_ball:", "🍙"}, + {":rice_cracker:", "🍘"}, + {":rice:", "🍚"}, + {":ramen:", "🍜"}, + {":stew:", "🍲"}, + {":oden:", "🍢"}, + {":dango:", "🍡"}, + {":egg:", "🍳"}, + {":bread:", "🍞"}, + {":doughnut:", "🍩"}, + {":custard:", "🍮"}, + {":icecream:", "🍦"}, + {":ice_cream:", "🍨"}, + {":shaved_ice:", "🍧"}, + {":birthday:", "🎂"}, + {":cake:", "🍰"}, + {":cookie:", "🍪"}, + {":chocolate_bar:", "🍫"}, + {":candy:", "🍬"}, + {":lollipop:", "🍭"}, + {":honey_pot:", "🍯"}, + {":apple:", "🍎"}, + {":green_apple:", "🍏"}, + {":tangerine:", "🍊"}, + {":lemon:", "🍋"}, + {":cherries:", "🍒"}, + {":grapes:", "🍇"}, + {":watermelon:", "🍉"}, + {":strawberry:", "🍓"}, + {":peach:", "🍑"}, + {":melon:", "🍈"}, + {":banana:", "🍌"}, + {":pear:", "🍐"}, + {":pineapple:", "🍍"}, + {":sweet_potato:", "🍠"}, + {":eggplant:", "🍆"}, + {":tomato:", "🍅"}, + {":corn:", "🌽"}, + {":house:", "🏠"}, + {":house_with_garden:", "🏡"}, + {":school:", "🏫"}, + {":office:", "🏢"}, + {":post_office:", "🏣"}, + {":hospital:", "🏥"}, + {":bank:", "🏦"}, + {":convenience_store:", "🏪"}, + {":love_hotel:", "🏩"}, + {":hotel:", "🏨"}, + {":wedding:", "💒"}, + {":church:", "⛪"}, + {":department_store:", "🏬"}, + {":european_post_office:", "🏤"}, + {":city_sunrise:", "🌇"}, + {":city_sunset:", "🌆"}, + {":japanese_castle:", "🏯"}, + {":european_castle:", "🏰"}, + {":tent:", "⛺"}, + {":factory:", "🏭"}, + {":tokyo_tower:", "🗼"}, + {":japan:", "🗾"}, + {":mount_fuji:", "🗻"}, + {":sunrise_over_mountains:", "🌄"}, + {":sunrise:", "🌅"}, + {":night_with_stars:", "🌃"}, + {":statue_of_liberty:", "🗽"}, + {":bridge_at_night:", "🌉"}, + {":carousel_horse:", "🎠"}, + {":ferris_wheel:", "🎡"}, + {":fountain:", "⛲"}, + {":roller_coaster:", "🎢"}, + {":ship:", "🚢"}, + {":boat:", "⛵"}, + {":sailboat:", "⛵"}, + {":speedboat:", "🚤"}, + {":rowboat:", "🚣"}, + {":anchor:", "⚓"}, + {":rocket:", "🚀"}, + {":airplane:", "✈️"}, + {":seat:", "💺"}, + {":helicopter:", "🚁"}, + {":steam_locomotive:", "🚂"}, + {":tram:", "🚊"}, + {":station:", "🚉"}, + {":mountain_railway:", "🚞"}, + {":train2:", "🚆"}, + {":bullettrain_side:", "🚄"}, + {":bullettrain_front:", "🚅"}, + {":light_rail:", "🚈"}, + {":metro:", "🚇"}, + {":monorail:", "🚝"}, + {":train:", "🚋"}, + {":railway_car:", "🚃"}, + {":trolleybus:", "🚎"}, + {":bus:", "🚌"}, + {":oncoming_bus:", "🚍"}, + {":blue_car:", "🚙"}, + {":oncoming_automobile:", "🚘"}, + {":car:", "🚗"}, + {":red_car:", "🚗"}, + {":taxi:", "🚕"}, + {":oncoming_taxi:", "🚖"}, + {":articulated_lorry:", "🚛"}, + {":truck:", "🚚"}, + {":rotating_light:", "🚨"}, + {":police_car:", "🚓"}, + {":oncoming_police_car:", "🚔"}, + {":fire_engine:", "🚒"}, + {":ambulance:", "🚑"}, + {":minibus:", "🚐"}, + {":bike:", "🚲"}, + {":aerial_tramway:", "🚡"}, + {":suspension_railway:", "🚟"}, + {":mountain_cableway:", "🚠"}, + {":tractor:", "🚜"}, + {":barber:", "💈"}, + {":busstop:", "🚏"}, + {":ticket:", "🎫"}, + {":vertical_traffic_light:", "🚦"}, + {":traffic_light:", "🚥"}, + {":warning:", "⚠️"}, + {":construction:", "🚧"}, + {":beginner:", "🔰"}, + {":fuelpump:", "⛽"}, + {":izakaya_lantern:", "🏮"}, + {":lantern:", "🏮"}, + {":slot_machine:", "🎰"}, + {":hotsprings:", "♨️"}, + {":moyai:", "🗿"}, + {":circus_tent:", "🎪"}, + {":performing_arts:", "🎭"}, + {":round_pushpin:", "📍"}, + {":triangular_flag_on_post:", "🚩"}, + {":jp:", "🇯🇵"}, + {":kr:", "🇰🇷"}, + {":de:", "🇩🇪"}, + {":cn:", "🇨🇳"}, + {":us:", "🇺🇸"}, + {":fr:", "🇫🇷"}, + {":es:", "🇪🇸"}, + {":it:", "🇮🇹"}, + {":ru:", "🇷🇺"}, + {":gb:", "🇬🇧"}, + {":uk:", "🇬🇧"}, + {":one:", "1️⃣"}, + {":two:", "2️⃣"}, + {":three:", "3️⃣"}, + {":four:", "4️⃣"}, + {":five:", "5️⃣"}, + {":six:", "6️⃣"}, + {":seven:", "7️⃣"}, + {":eight:", "8️⃣"}, + {":nine:", "9️⃣"}, + {":zero:", "0️⃣"}, + {":keycap_ten:", "🔟"}, + {":hash:", "#️⃣"}, + {":symbols:", "🔣"}, + {":arrow_up:", "⬆️"}, + {":arrow_down:", "⬇️"}, + {":arrow_left:", "⬅️"}, + {":arrow_right:", "➡️"}, + {":capital_abcd:", "🔠"}, + {":abcd:", "🔡"}, + {":abc:", "🔤"}, + {":arrow_upper_right:", "↗️"}, + {":arrow_upper_left:", "↖️"}, + {":arrow_lower_right:", "↘️"}, + {":arrow_lower_left:", "↙️"}, + {":left_right_arrow:", "↔️"}, + {":arrow_up_down:", "↕️"}, + {":arrows_counterclockwise:", "🔄"}, + {":arrow_backward:", "◀️"}, + {":arrow_forward:", "▶️"}, + {":arrow_up_small:", "🔼"}, + {":arrow_down_small:", "🔽"}, + {":leftwards_arrow_with_hook:", "↩️"}, + {":arrow_right_hook:", "↪️"}, + {":information_source:", "ℹ️"}, + {":rewind:", "⏪"}, + {":fast_forward:", "⏩"}, + {":arrow_double_up:", "⏫"}, + {":arrow_double_down:", "⏬"}, + {":arrow_heading_down:", "⤵️"}, + {":arrow_heading_up:", "⤴️"}, + {":ok:", "🆗"}, + {":twisted_rightwards_arrows:", "🔀"}, + {":repeat:", "🔁"}, + {":repeat_one:", "🔂"}, + {":new:", "🆕"}, + {":up:", "🆙"}, + {":cool:", "🆒"}, + {":free:", "🆓"}, + {":ng:", "🆖"}, + {":signal_strength:", "📶"}, + {":cinema:", "🎦"}, + {":koko:", "🈁"}, + {":u6307:", "🈯"}, + {":u7a7a:", "🈳"}, + {":u6e80:", "🈵"}, + {":u5408:", "🈴"}, + {":u7981:", "🈲"}, + {":ideograph_advantage:", "🉐"}, + {":u5272:", "🈹"}, + {":u55b6:", "🈺"}, + {":u6709:", "🈶"}, + {":u7121:", "🈚"}, + {":restroom:", "🚻"}, + {":mens:", "🚹"}, + {":womens:", "🚺"}, + {":baby_symbol:", "🚼"}, + {":wc:", "🚾"}, + {":potable_water:", "🚰"}, + {":put_litter_in_its_place:", "🚮"}, + {":parking:", "🅿️"}, + {":wheelchair:", "♿"}, + {":no_smoking:", "🚭"}, + {":u6708:", "🈷️"}, + {":u7533:", "🈸"}, + {":sa:", "🈂️"}, + {":m:", "Ⓜ️"}, + {":passport_control:", "🛂"}, + {":baggage_claim:", "🛄"}, + {":left_luggage:", "🛅"}, + {":customs:", "🛃"}, + {":accept:", "🉑"}, + {":secret:", "㊙️"}, + {":congratulations:", "㊗️"}, + {":cl:", "🆑"}, + {":sos:", "🆘"}, + {":id:", "🆔"}, + {":no_entry_sign:", "🚫"}, + {":underage:", "🔞"}, + {":no_mobile_phones:", "📵"}, + {":do_not_litter:", "🚯"}, + {":non-potable_water:", "🚱"}, + {":no_bicycles:", "🚳"}, + {":no_pedestrians:", "🚷"}, + {":children_crossing:", "🚸"}, + {":no_entry:", "⛔"}, + {":eight_spoked_asterisk:", "✳️"}, + {":sparkle:", "❇️"}, + {":negative_squared_cross_mark:", "❎"}, + {":white_check_mark:", "✅"}, + {":eight_pointed_black_star:", "✴️"}, + {":heart_decoration:", "💟"}, + {":vs:", "🆚"}, + {":vibration_mode:", "📳"}, + {":mobile_phone_off:", "📴"}, + {":a:", "🅰️"}, + {":b:", "🅱️"}, + {":ab:", "🆎"}, + {":o2:", "🅾️"}, + {":diamond_shape_with_a_dot_inside:", "💠"}, + {":loop:", "➿"}, + {":recycle:", "♻️"}, + {":aries:", "♈"}, + {":taurus:", "♉"}, + {":gemini:", "♊"}, + {":cancer:", "♋"}, + {":leo:", "♌"}, + {":virgo:", "♍"}, + {":libra:", "♎"}, + {":scorpius:", "♏"}, + {":sagittarius:", "♐"}, + {":capricorn:", "♑"}, + {":aquarius:", "♒"}, + {":pisces:", "♓"}, + {":ophiuchus:", "⛎"}, + {":six_pointed_star:", "🔯"}, + {":atm:", "🏧"}, + {":chart:", "💹"}, + {":heavy_dollar_sign:", "💲"}, + {":currency_exchange:", "💱"}, + {":copyright:", "©️"}, + {":registered:", "®️"}, + {":tm:", "™️"}, + {":x:", "❌"}, + {":bangbang:", "‼️"}, + {":interrobang:", "⁉️"}, + {":exclamation:", "❗"}, + {":heavy_exclamation_mark:", "❗"}, + {":question:", "❓"}, + {":grey_exclamation:", "❕"}, + {":grey_question:", "❔"}, + {":o:", "⭕"}, + {":top:", "🔝"}, + {":end:", "🔚"}, + {":back:", "🔙"}, + {":on:", "🔛"}, + {":soon:", "🔜"}, + {":arrows_clockwise:", "🔃"}, + {":clock12:", "🕛"}, + {":clock1230:", "🕧"}, + {":clock1:", "🕐"}, + {":clock130:", "🕜"}, + {":clock2:", "🕑"}, + {":clock230:", "🕝"}, + {":clock3:", "🕒"}, + {":clock330:", "🕞"}, + {":clock4:", "🕓"}, + {":clock430:", "🕟"}, + {":clock5:", "🕔"}, + {":clock530:", "🕠"}, + {":clock6:", "🕕"}, + {":clock7:", "🕖"}, + {":clock8:", "🕗"}, + {":clock9:", "🕘"}, + {":clock10:", "🕙"}, + {":clock11:", "🕚"}, + {":clock630:", "🕡"}, + {":clock730:", "🕢"}, + {":clock830:", "🕣"}, + {":clock930:", "🕤"}, + {":clock1030:", "🕥"}, + {":clock1130:", "🕦"}, + {":heavy_multiplication_x:", "✖️"}, + {":heavy_plus_sign:", "➕"}, + {":heavy_minus_sign:", "➖"}, + {":heavy_division_sign:", "➗"}, + {":spades:", "♠️"}, + {":hearts:", "♥️"}, + {":clubs:", "♣️"}, + {":diamonds:", "♦️"}, + {":white_flower:", "💮"}, + {":heavy_check_mark:", "✔️"}, + {":ballot_box_with_check:", "☑️"}, + {":radio_button:", "🔘"}, + {":link:", "🔗"}, + {":curly_loop:", "➰"}, + {":wavy_dash:", "〰️"}, + {":part_alternation_mark:", "〽️"}, + {":trident:", "🔱"}, + {":black_medium_square:", "◼️"}, + {":white_medium_square:", "◻️"}, + {":black_medium_small_square:", "◾"}, + {":white_medium_small_square:", "◽"}, + {":black_small_square:", "▪️"}, + {":white_small_square:", "▫️"}, + {":small_red_triangle:", "🔺"}, + {":black_square_button:", "🔲"}, + {":white_square_button:", "🔳"}, + {":black_circle:", "⚫"}, + {":white_circle:", "⚪"}, + {":red_circle:", "🔴"}, + {":large_blue_circle:", "🔵"}, + {":small_red_triangle_down:", "🔻"}, + {":white_large_square:", "⬜"}, + {":black_large_square:", "⬛"}, + {":large_orange_diamond:", "🔶"}, + {":large_blue_diamond:", "🔷"}, + {":small_orange_diamond:", "🔸"}, + {":small_blue_diamond:", "🔹"}, - // Custom additions - { ":custom_arrow_left:", "←"}, - { ":custom_arrow_right:", "→"}, - { ":custom_arrow_left_right:", "↔"}, + // Custom additions + { ":custom_arrow_left:", "←"}, + { ":custom_arrow_right:", "→"}, + { ":custom_arrow_left_right:", "↔"}, - { ":custom_arrow_left_strong:", "⇐"}, - { ":custom_arrow_right_strong:", "⇒"}, - { ":custom_arrow_left_right_strong:", "⇔"}, - {":rofl:","🤣"}, - {":slightly_smiling_face:","🙂"}, - {":upside_down_face:","🙃"}, - {":star_struck:","🤩"}, - {":zany_face:","🤪"}, - {":money_mouth_face:","🤑"}, - {":hugs:","🤗"}, - {":hand_over_mouth:","🤭"}, - {":shushing_face:","🤫"}, - {":thinking:","🤔"}, - {":zipper_mouth_face:","🤐"}, - {":raised_eyebrow:","🤨"}, - {":roll_eyes:","🙄"}, - {":lying_face:","🤥"}, - {":drooling_face:","🤤"}, - {":face_with_thermometer:","🤒"}, - {":face_with_head_bandage:","🤕"}, - {":nauseated_face:","🤢"}, - {":vomiting_face:","🤮"}, - {":sneezing_face:","🤧"}, - {":exploding_head:","🤯"}, - {":cowboy_hat_face:","🤠"}, - {":nerd_face:","🤓"}, - {":monocle_face:","🧐"}, - {":slightly_frowning_face:","🙁"}, - {":frowning_face:","☹️"}, - {":cursing_face:","🤬"}, - {":skull_and_crossbones:","☠️"}, - {":clown_face:","🤡"}, - {":robot:","🤖"}, - {":heavy_heart_exclamation:","❣️"}, - {":orange_heart:","🧡"}, - {":black_heart:","🖤"}, - {":hole:","🕳️"}, - {":eye_speech_bubble:","👁️‍🗨️"}, - {":left_speech_bubble:","🗨️"}, - {":right_anger_bubble:","🗯️"}, - {":raised_back_of_hand:","🤚"}, - {":raised_hand_with_fingers_splayed:","🖐️"}, - {":vulcan_salute:","🖖"}, - {":crossed_fingers:","🤞"}, - {":love_you_gesture:","🤟"}, - {":metal:","🤘"}, - {":call_me_hand:","🤙"}, - {":middle_finger:","🖕"}, - {":fist_raised:","✊"}, - {":fist_oncoming:","👊"}, - {":fist_left:","🤛"}, - {":fist_right:","🤜"}, - {":palms_up_together:","🤲"}, - {":handshake:","🤝"}, - {":writing_hand:","✍️"}, - {":selfie:","🤳"}, - {":brain:","🧠"}, - {":eye:","👁️"}, - {":child:","🧒"}, - {":adult:","🧑"}, - {":blond_haired_person:","👱"}, - {":bearded_person:","🧔"}, - {":blond_haired_man:","👱‍♂️"}, - {":blond_haired_woman:","👱‍♀️"}, - {":older_adult:","🧓"}, - {":frowning_person:","🙍"}, - {":frowning_man:","🙍‍♂️"}, - {":frowning_woman:","🙍‍♀️"}, - {":pouting_face:","🙎"}, - {":pouting_man:","🙎‍♂️"}, - {":pouting_woman:","🙎‍♀️"}, - {":no_good_man:","🙅‍♂️"}, - {":no_good_woman:","🙅‍♀️"}, - {":ok_person:","🙆"}, - {":ok_man:","🙆‍♂️"}, - {":tipping_hand_person:","💁"}, - {":tipping_hand_man:","💁‍♂️"}, - {":tipping_hand_woman:","💁‍♀️"}, - {":raising_hand_man:","🙋‍♂️"}, - {":raising_hand_woman:","🙋‍♀️"}, - {":bowing_man:","🙇‍♂️"}, - {":bowing_woman:","🙇‍♀️"}, - {":facepalm:","🤦"}, - {":man_facepalming:","🤦‍♂️"}, - {":woman_facepalming:","🤦‍♀️"}, - {":shrug:","🤷"}, - {":man_shrugging:","🤷‍♂️"}, - {":woman_shrugging:","🤷‍♀️"}, - {":man_health_worker:","👨‍⚕️"}, - {":woman_health_worker:","👩‍⚕️"}, - {":man_student:","👨‍🎓"}, - {":woman_student:","👩‍🎓"}, - {":man_teacher:","👨‍🏫"}, - {":woman_teacher:","👩‍🏫"}, - {":man_judge:","👨‍⚖️"}, - {":woman_judge:","👩‍⚖️"}, - {":man_farmer:","👨‍🌾"}, - {":woman_farmer:","👩‍🌾"}, - {":man_cook:","👨‍🍳"}, - {":woman_cook:","👩‍🍳"}, - {":man_mechanic:","👨‍🔧"}, - {":woman_mechanic:","👩‍🔧"}, - {":man_factory_worker:","👨‍🏭"}, - {":woman_factory_worker:","👩‍🏭"}, - {":man_office_worker:","👨‍💼"}, - {":woman_office_worker:","👩‍💼"}, - {":man_scientist:","👨‍🔬"}, - {":woman_scientist:","👩‍🔬"}, - {":man_technologist:","👨‍💻"}, - {":woman_technologist:","👩‍💻"}, - {":man_singer:","👨‍🎤"}, - {":woman_singer:","👩‍🎤"}, - {":man_artist:","👨‍🎨"}, - {":woman_artist:","👩‍🎨"}, - {":man_pilot:","👨‍✈️"}, - {":woman_pilot:","👩‍✈️"}, - {":man_astronaut:","👨‍🚀"}, - {":woman_astronaut:","👩‍🚀"}, - {":man_firefighter:","👨‍🚒"}, - {":woman_firefighter:","👩‍🚒"}, - {":police_officer:","👮"}, - {":policeman:","👮‍♂️"}, - {":policewoman:","👮‍♀️"}, - {":detective:","🕵️"}, - {":male_detective:","🕵️‍♂️"}, - {":female_detective:","🕵️‍♀️"}, - {":guard:","💂"}, - {":guardswoman:","💂‍♀️"}, - {":construction_worker_man:","👷‍♂️"}, - {":construction_worker_woman:","👷‍♀️"}, - {":prince:","🤴"}, - {":person_with_turban:","👳"}, - {":woman_with_turban:","👳‍♀️"}, - {":woman_with_headscarf:","🧕"}, - {":man_in_tuxedo:","🤵"}, - {":pregnant_woman:","🤰"}, - {":breast_feeding:","🤱"}, - {":mrs_claus:","🤶"}, - {":mage:","🧙"}, - {":mage_man:","🧙‍♂️"}, - {":mage_woman:","🧙‍♀️"}, - {":fairy:","🧚"}, - {":fairy_man:","🧚‍♂️"}, - {":fairy_woman:","🧚‍♀️"}, - {":vampire:","🧛"}, - {":vampire_man:","🧛‍♂️"}, - {":vampire_woman:","🧛‍♀️"}, - {":merperson:","🧜"}, - {":merman:","🧜‍♂️"}, - {":mermaid:","🧜‍♀️"}, - {":elf:","🧝"}, - {":elf_man:","🧝‍♂️"}, - {":elf_woman:","🧝‍♀️"}, - {":genie:","🧞"}, - {":genie_man:","🧞‍♂️"}, - {":genie_woman:","🧞‍♀️"}, - {":zombie:","🧟"}, - {":zombie_man:","🧟‍♂️"}, - {":zombie_woman:","🧟‍♀️"}, - {":massage_man:","💆‍♂️"}, - {":massage_woman:","💆‍♀️"}, - {":haircut_man:","💇‍♂️"}, - {":haircut_woman:","💇‍♀️"}, - {":walking_man:","🚶‍♂️"}, - {":walking_woman:","🚶‍♀️"}, - {":running_man:","🏃‍♂️"}, - {":running_woman:","🏃‍♀️"}, - {":woman_dancing:","💃"}, - {":man_dancing:","🕺"}, - {":business_suit_levitating:","🕴️"}, - {":dancing_men:","👯‍♂️"}, - {":dancing_women:","👯‍♀️"}, - {":sauna_person:","🧖"}, - {":sauna_man:","🧖‍♂️"}, - {":sauna_woman:","🧖‍♀️"}, - {":climbing:","🧗"}, - {":climbing_man:","🧗‍♂️"}, - {":climbing_woman:","🧗‍♀️"}, - {":person_fencing:","🤺"}, - {":skier:","⛷️"}, - {":golfing:","🏌️"}, - {":golfing_man:","🏌️‍♂️"}, - {":golfing_woman:","🏌️‍♀️"}, - {":surfing_man:","🏄‍♂️"}, - {":surfing_woman:","🏄‍♀️"}, - {":rowing_man:","🚣‍♂️"}, - {":rowing_woman:","🚣‍♀️"}, - {":swimming_man:","🏊‍♂️"}, - {":swimming_woman:","🏊‍♀️"}, - {":bouncing_ball_person:","⛹️"}, - {":bouncing_ball_man:","⛹️‍♂️"}, - {":bouncing_ball_woman:","⛹️‍♀️"}, - {":weight_lifting:","🏋️"}, - {":weight_lifting_man:","🏋️‍♂️"}, - {":weight_lifting_woman:","🏋️‍♀️"}, - {":biking_man:","🚴‍♂️"}, - {":biking_woman:","🚴‍♀️"}, - {":mountain_biking_man:","🚵‍♂️"}, - {":mountain_biking_woman:","🚵‍♀️"}, - {":cartwheeling:","🤸"}, - {":man_cartwheeling:","🤸‍♂️"}, - {":woman_cartwheeling:","🤸‍♀️"}, - {":wrestling:","🤼"}, - {":men_wrestling:","🤼‍♂️"}, - {":women_wrestling:","🤼‍♀️"}, - {":water_polo:","🤽"}, - {":man_playing_water_polo:","🤽‍♂️"}, - {":woman_playing_water_polo:","🤽‍♀️"}, - {":handball_person:","🤾"}, - {":man_playing_handball:","🤾‍♂️"}, - {":woman_playing_handball:","🤾‍♀️"}, - {":juggling_person:","🤹"}, - {":man_juggling:","🤹‍♂️"}, - {":woman_juggling:","🤹‍♀️"}, - {":lotus_position:","🧘"}, - {":lotus_position_man:","🧘‍♂️"}, - {":lotus_position_woman:","🧘‍♀️"}, - {":sleeping_bed:","🛌"}, - {":people_holding_hands:","🧑‍🤝‍🧑"}, - {":couplekiss_man_woman:","👩‍❤️‍💋‍👨"}, - {":couplekiss_man_man:","👨‍❤️‍💋‍👨"}, - {":couplekiss_woman_woman:","👩‍❤️‍💋‍👩"}, - {":couple_with_heart_woman_man:","👩‍❤️‍👨"}, - {":couple_with_heart_man_man:","👨‍❤️‍👨"}, - {":couple_with_heart_woman_woman:","👩‍❤️‍👩"}, - {":family_man_woman_boy:","👨‍👩‍👦"}, - {":family_man_woman_girl:","👨‍👩‍👧"}, - {":family_man_woman_girl_boy:","👨‍👩‍👧‍👦"}, - {":family_man_woman_boy_boy:","👨‍👩‍👦‍👦"}, - {":family_man_woman_girl_girl:","👨‍👩‍👧‍👧"}, - {":family_man_man_boy:","👨‍👨‍👦"}, - {":family_man_man_girl:","👨‍👨‍👧"}, - {":family_man_man_girl_boy:","👨‍👨‍👧‍👦"}, - {":family_man_man_boy_boy:","👨‍👨‍👦‍👦"}, - {":family_man_man_girl_girl:","👨‍👨‍👧‍👧"}, - {":family_woman_woman_boy:","👩‍👩‍👦"}, - {":family_woman_woman_girl:","👩‍👩‍👧"}, - {":family_woman_woman_girl_boy:","👩‍👩‍👧‍👦"}, - {":family_woman_woman_boy_boy:","👩‍👩‍👦‍👦"}, - {":family_woman_woman_girl_girl:","👩‍👩‍👧‍👧"}, - {":family_man_boy:","👨‍👦"}, - {":family_man_boy_boy:","👨‍👦‍👦"}, - {":family_man_girl:","👨‍👧"}, - {":family_man_girl_boy:","👨‍👧‍👦"}, - {":family_man_girl_girl:","👨‍👧‍👧"}, - {":family_woman_boy:","👩‍👦"}, - {":family_woman_boy_boy:","👩‍👦‍👦"}, - {":family_woman_girl:","👩‍👧"}, - {":family_woman_girl_boy:","👩‍👧‍👦"}, - {":family_woman_girl_girl:","👩‍👧‍👧"}, - {":speaking_head:","🗣️"}, - {":gorilla:","🦍"}, - {":fox_face:","🦊"}, - {":lion:","🦁"}, - {":unicorn:","🦄"}, - {":zebra:","🦓"}, - {":deer:","🦌"}, - {":giraffe:","🦒"}, - {":rhinoceros:","🦏"}, - {":chipmunk:","🐿️"}, - {":hedgehog:","🦔"}, - {":bat:","🦇"}, - {":turkey:","🦃"}, - {":dove:","🕊️"}, - {":eagle:","🦅"}, - {":duck:","🦆"}, - {":owl:","🦉"}, - {":lizard:","🦎"}, - {":sauropod:","🦕"}, - {":t-rex:","🦖"}, - {":shark:","🦈"}, - {":butterfly:","🦋"}, - {":cricket:","🦗"}, - {":spider:","🕷️"}, - {":spider_web:","🕸️"}, - {":scorpion:","🦂"}, - {":rosette:","🏵️"}, - {":wilted_flower:","🥀"}, - {":shamrock:","☘️"}, - {":kiwi_fruit:","🥝"}, - {":coconut:","🥥"}, - {":avocado:","🥑"}, - {":potato:","🥔"}, - {":carrot:","🥕"}, - {":hot_pepper:","🌶️"}, - {":cucumber:","🥒"}, - {":broccoli:","🥦"}, - {":peanuts:","🥜"}, - {":croissant:","🥐"}, - {":baguette_bread:","🥖"}, - {":pretzel:","🥨"}, - {":pancakes:","🥞"}, - {":cheese:","🧀"}, - {":cut_of_meat:","🥩"}, - {":bacon:","🥓"}, - {":hotdog:","🌭"}, - {":sandwich:","🥪"}, - {":taco:","🌮"}, - {":burrito:","🌯"}, - {":stuffed_flatbread:","🥙"}, - {":fried_egg:","🍳"}, - {":shallow_pan_of_food:","🥘"}, - {":bowl_with_spoon:","🥣"}, - {":green_salad:","🥗"}, - {":popcorn:","🍿"}, - {":canned_food:","🥫"}, - {":dumpling:","🥟"}, - {":fortune_cookie:","🥠"}, - {":takeout_box:","🥡"}, - {":crab:","🦀"}, - {":shrimp:","🦐"}, - {":squid:","🦑"}, - {":pie:","🥧"}, - {":milk_glass:","🥛"}, - {":champagne:","🍾"}, - {":clinking_glasses:","🥂"}, - {":tumbler_glass:","🥃"}, - {":cup_with_straw:","🥤"}, - {":chopsticks:","🥢"}, - {":plate_with_cutlery:","🍽️"}, - {":spoon:","🥄"}, - {":amphora:","🏺"}, - {":world_map:","🗺️"}, - {":mountain_snow:","🏔️"}, - {":mountain:","⛰️"}, - {":camping:","🏕️"}, - {":beach_umbrella:","🏖️"}, - {":desert:","🏜️"}, - {":desert_island:","🏝️"}, - {":national_park:","🏞️"}, - {":stadium:","🏟️"}, - {":classical_building:","🏛️"}, - {":building_construction:","🏗️"}, - {":houses:","🏘️"}, - {":derelict_house:","🏚️"}, - {":mosque:","🕌"}, - {":synagogue:","🕍"}, - {":shinto_shrine:","⛩️"}, - {":kaaba:","🕋"}, - {":cityscape:","🏙️"}, - {":racing_car:","🏎️"}, - {":motorcycle:","🏍️"}, - {":motor_scooter:","🛵"}, - {":kick_scooter:","🛴"}, - {":motorway:","🛣️"}, - {":railway_track:","🛤️"}, - {":oil_drum:","🛢️"}, - {":stop_sign:","🛑"}, - {":canoe:","🛶"}, - {":passenger_ship:","🛳️"}, - {":ferry:","⛴️"}, - {":motor_boat:","🛥️"}, - {":small_airplane:","🛩️"}, - {":flight_departure:","🛫"}, - {":flight_arrival:","🛬"}, - {":artificial_satellite:","🛰️"}, - {":flying_saucer:","🛸"}, - {":bellhop_bell:","🛎️"}, - {":stopwatch:","⏱️"}, - {":timer_clock:","⏲️"}, - {":mantelpiece_clock:","🕰️"}, - {":thermometer:","🌡️"}, - {":cloud_with_lightning_and_rain:","⛈️"}, - {":sun_behind_small_cloud:","🌤️"}, - {":sun_behind_large_cloud:","🌥️"}, - {":sun_behind_rain_cloud:","🌦️"}, - {":cloud_with_rain:","🌧️"}, - {":cloud_with_snow:","🌨️"}, - {":cloud_with_lightning:","🌩️"}, - {":tornado:","🌪️"}, - {":fog:","🌫️"}, - {":wind_face:","🌬️"}, - {":open_umbrella:","☂️"}, - {":parasol_on_ground:","⛱️"}, - {":snowman_with_snow:","☃️"}, - {":comet:","☄️"}, - {":reminder_ribbon:","🎗️"}, - {":tickets:","🎟️"}, - {":medal_military:","🎖️"}, - {":medal_sports:","🏅"}, - {":1st_place_medal:","🥇"}, - {":2nd_place_medal:","🥈"}, - {":3rd_place_medal:","🥉"}, - {":volleyball:","🏐"}, - {":cricket_game:","🏏"}, - {":field_hockey:","🏑"}, - {":ice_hockey:","🏒"}, - {":ping_pong:","🏓"}, - {":badminton:","🏸"}, - {":boxing_glove:","🥊"}, - {":martial_arts_uniform:","🥋"}, - {":goal_net:","🥅"}, - {":ice_skate:","⛸️"}, - {":sled:","🛷"}, - {":curling_stone:","🥌"}, - {":joystick:","🕹️"}, - {":chess_pawn:","♟️"}, - {":framed_picture:","🖼️"}, - {":dark_sunglasses:","🕶️"}, - {":scarf:","🧣"}, - {":gloves:","🧤"}, - {":coat:","🧥"}, - {":socks:","🧦"}, - {":shopping:","🛍️"}, - {":billed_cap:","🧢"}, - {":rescue_worker_helmet:","⛑️"}, - {":prayer_beads:","📿"}, - {":studio_microphone:","🎙️"}, - {":level_slider:","🎚️"}, - {":control_knobs:","🎛️"}, - {":drum:","🥁"}, - {":desktop_computer:","🖥️"}, - {":printer:","🖨️"}, - {":keyboard:","⌨️"}, - {":computer_mouse:","🖱️"}, - {":trackball:","🖲️"}, - {":film_strip:","🎞️"}, - {":film_projector:","📽️"}, - {":camera_flash:","📸"}, - {":candle:","🕯️"}, - {":newspaper_roll:","🗞️"}, - {":label:","🏷️"}, - {":ballot_box:","🗳️"}, - {":fountain_pen:","🖋️"}, - {":pen:","🖊️"}, - {":paintbrush:","🖌️"}, - {":crayon:","🖍️"}, - {":card_index_dividers:","🗂️"}, - {":spiral_notepad:","🗒️"}, - {":spiral_calendar:","🗓️"}, - {":paperclips:","🖇️"}, - {":card_file_box:","🗃️"}, - {":file_cabinet:","🗄️"}, - {":wastebasket:","🗑️"}, - {":old_key:","🗝️"}, - {":pick:","⛏️"}, - {":hammer_and_pick:","⚒️"}, - {":hammer_and_wrench:","🛠️"}, - {":dagger:","🗡️"}, - {":crossed_swords:","⚔️"}, - {":bow_and_arrow:","🏹"}, - {":shield:","🛡️"}, - {":gear:","⚙️"}, - {":clamp:","🗜️"}, - {":balance_scale:","⚖️"}, - {":chains:","⛓️"}, - {":alembic:","⚗️"}, - {":bed:","🛏️"}, - {":couch_and_lamp:","🛋️"}, - {":shopping_cart:","🛒"}, - {":coffin:","⚰️"}, - {":funeral_urn:","⚱️"}, - {":radioactive:","☢️"}, - {":biohazard:","☣️"}, - {":place_of_worship:","🛐"}, - {":atom_symbol:","⚛️"}, - {":om:","🕉️"}, - {":star_of_david:","✡️"}, - {":wheel_of_dharma:","☸️"}, - {":yin_yang:","☯️"}, - {":latin_cross:","✝️"}, - {":orthodox_cross:","☦️"}, - {":star_and_crescent:","☪️"}, - {":peace_symbol:","☮️"}, - {":menorah:","🕎"}, - {":next_track_button:","⏭️"}, - {":play_or_pause_button:","⏯️"}, - {":previous_track_button:","⏮️"}, - {":pause_button:","⏸️"}, - {":stop_button:","⏹️"}, - {":record_button:","⏺️"}, - {":eject_button:","⏏️"}, - {":female_sign:","♀️"}, - {":male_sign:","♂️"}, - {":medical_symbol:","⚕️"}, - {":infinity:","♾️"}, - {":fleur_de_lis:","⚜️"}, - {":asterisk:","*️⃣"}, - {":black_flag:","🏴"}, - {":white_flag:","🏳️"}, - {":rainbow_flag:","🏳️‍🌈"}, - {":pirate_flag:","🏴‍☠️"}, - {":ascension_island:","🇦🇨"}, - {":andorra:","🇦🇩"}, - {":united_arab_emirates:","🇦🇪"}, - {":afghanistan:","🇦🇫"}, - {":antigua_barbuda:","🇦🇬"}, - {":anguilla:","🇦🇮"}, - {":albania:","🇦🇱"}, - {":armenia:","🇦🇲"}, - {":angola:","🇦🇴"}, - {":antarctica:","🇦🇶"}, - {":argentina:","🇦🇷"}, - {":american_samoa:","🇦🇸"}, - {":austria:","🇦🇹"}, - {":australia:","🇦🇺"}, - {":aruba:","🇦🇼"}, - {":aland_islands:","🇦🇽"}, - {":azerbaijan:","🇦🇿"}, - {":bosnia_herzegovina:","🇧🇦"}, - {":barbados:","🇧🇧"}, - {":bangladesh:","🇧🇩"}, - {":belgium:","🇧🇪"}, - {":burkina_faso:","🇧🇫"}, - {":bulgaria:","🇧🇬"}, - {":bahrain:","🇧🇭"}, - {":burundi:","🇧🇮"}, - {":benin:","🇧🇯"}, - {":st_barthelemy:","🇧🇱"}, - {":bermuda:","🇧🇲"}, - {":brunei:","🇧🇳"}, - {":bolivia:","🇧🇴"}, - {":caribbean_netherlands:","🇧🇶"}, - {":brazil:","🇧🇷"}, - {":bahamas:","🇧🇸"}, - {":bhutan:","🇧🇹"}, - {":bouvet_island:","🇧🇻"}, - {":botswana:","🇧🇼"}, - {":belarus:","🇧🇾"}, - {":belize:","🇧🇿"}, - {":canada:","🇨🇦"}, - {":cocos_islands:","🇨🇨"}, - {":congo_kinshasa:","🇨🇩"}, - {":central_african_republic:","🇨🇫"}, - {":congo_brazzaville:","🇨🇬"}, - {":switzerland:","🇨🇭"}, - {":cote_divoire:","🇨🇮"}, - {":cook_islands:","🇨🇰"}, - {":chile:","🇨🇱"}, - {":cameroon:","🇨🇲"}, - {":colombia:","🇨🇴"}, - {":clipperton_island:","🇨🇵"}, - {":costa_rica:","🇨🇷"}, - {":cuba:","🇨🇺"}, - {":cape_verde:","🇨🇻"}, - {":curacao:","🇨🇼"}, - {":christmas_island:","🇨🇽"}, - {":cyprus:","🇨🇾"}, - {":czech_republic:","🇨🇿"}, - {":diego_garcia:","🇩🇬"}, - {":djibouti:","🇩🇯"}, - {":denmark:","🇩🇰"}, - {":dominica:","🇩🇲"}, - {":dominican_republic:","🇩🇴"}, - {":algeria:","🇩🇿"}, - {":ceuta_melilla:","🇪🇦"}, - {":ecuador:","🇪🇨"}, - {":estonia:","🇪🇪"}, - {":egypt:","🇪🇬"}, - {":western_sahara:","🇪🇭"}, - {":eritrea:","🇪🇷"}, - {":ethiopia:","🇪🇹"}, - {":eu:","🇪🇺"}, - {":finland:","🇫🇮"}, - {":fiji:","🇫🇯"}, - {":falkland_islands:","🇫🇰"}, - {":micronesia:","🇫🇲"}, - {":faroe_islands:","🇫🇴"}, - {":gabon:","🇬🇦"}, - {":grenada:","🇬🇩"}, - {":georgia:","🇬🇪"}, - {":french_guiana:","🇬🇫"}, - {":guernsey:","🇬🇬"}, - {":ghana:","🇬🇭"}, - {":gibraltar:","🇬🇮"}, - {":greenland:","🇬🇱"}, - {":gambia:","🇬🇲"}, - {":guinea:","🇬🇳"}, - {":guadeloupe:","🇬🇵"}, - {":equatorial_guinea:","🇬🇶"}, - {":greece:","🇬🇷"}, - {":south_georgia_south_sandwich_islands:","🇬🇸"}, - {":guatemala:","🇬🇹"}, - {":guam:","🇬🇺"}, - {":guinea_bissau:","🇬🇼"}, - {":guyana:","🇬🇾"}, - {":hong_kong:","🇭🇰"}, - {":heard_mcdonald_islands:","🇭🇲"}, - {":honduras:","🇭🇳"}, - {":croatia:","🇭🇷"}, - {":haiti:","🇭🇹"}, - {":hungary:","🇭🇺"}, - {":canary_islands:","🇮🇨"}, - {":indonesia:","🇮🇩"}, - {":ireland:","🇮🇪"}, - {":israel:","🇮🇱"}, - {":isle_of_man:","🇮🇲"}, - {":india:","🇮🇳"}, - {":british_indian_ocean_territory:","🇮🇴"}, - {":iraq:","🇮🇶"}, - {":iran:","🇮🇷"}, - {":iceland:","🇮🇸"}, - {":jersey:","🇯🇪"}, - {":jamaica:","🇯🇲"}, - {":jordan:","🇯🇴"}, - {":kenya:","🇰🇪"}, - {":kyrgyzstan:","🇰🇬"}, - {":cambodia:","🇰🇭"}, - {":kiribati:","🇰🇮"}, - {":comoros:","🇰🇲"}, - {":st_kitts_nevis:","🇰🇳"}, - {":north_korea:","🇰🇵"}, - {":kuwait:","🇰🇼"}, - {":cayman_islands:","🇰🇾"}, - {":kazakhstan:","🇰🇿"}, - {":laos:","🇱🇦"}, - {":lebanon:","🇱🇧"}, - {":st_lucia:","🇱🇨"}, - {":liechtenstein:","🇱🇮"}, - {":sri_lanka:","🇱🇰"}, - {":liberia:","🇱🇷"}, - {":lesotho:","🇱🇸"}, - {":lithuania:","🇱🇹"}, - {":luxembourg:","🇱🇺"}, - {":latvia:","🇱🇻"}, - {":libya:","🇱🇾"}, - {":morocco:","🇲🇦"}, - {":monaco:","🇲🇨"}, - {":moldova:","🇲🇩"}, - {":montenegro:","🇲🇪"}, - {":st_martin:","🇲🇫"}, - {":madagascar:","🇲🇬"}, - {":marshall_islands:","🇲🇭"}, - {":macedonia:","🇲🇰"}, - {":mali:","🇲🇱"}, - {":myanmar:","🇲🇲"}, - {":mongolia:","🇲🇳"}, - {":macau:","🇲🇴"}, - {":northern_mariana_islands:","🇲🇵"}, - {":martinique:","🇲🇶"}, - {":mauritania:","🇲🇷"}, - {":montserrat:","🇲🇸"}, - {":malta:","🇲🇹"}, - {":mauritius:","🇲🇺"}, - {":maldives:","🇲🇻"}, - {":malawi:","🇲🇼"}, - {":mexico:","🇲🇽"}, - {":malaysia:","🇲🇾"}, - {":mozambique:","🇲🇿"}, - {":namibia:","🇳🇦"}, - {":new_caledonia:","🇳🇨"}, - {":niger:","🇳🇪"}, - {":norfolk_island:","🇳🇫"}, - {":nigeria:","🇳🇬"}, - {":nicaragua:","🇳🇮"}, - {":netherlands:","🇳🇱"}, - {":norway:","🇳🇴"}, - {":nepal:","🇳🇵"}, - {":nauru:","🇳🇷"}, - {":niue:","🇳🇺"}, - {":new_zealand:","🇳🇿"}, - {":oman:","🇴🇲"}, - {":panama:","🇵🇦"}, - {":peru:","🇵🇪"}, - {":french_polynesia:","🇵🇫"}, - {":papua_new_guinea:","🇵🇬"}, - {":philippines:","🇵🇭"}, - {":pakistan:","🇵🇰"}, - {":poland:","🇵🇱"}, - {":st_pierre_miquelon:","🇵🇲"}, - {":pitcairn_islands:","🇵🇳"}, - {":puerto_rico:","🇵🇷"}, - {":palestinian_territories:","🇵🇸"}, - {":portugal:","🇵🇹"}, - {":palau:","🇵🇼"}, - {":paraguay:","🇵🇾"}, - {":qatar:","🇶🇦"}, - {":reunion:","🇷🇪"}, - {":romania:","🇷🇴"}, - {":serbia:","🇷🇸"}, - {":rwanda:","🇷🇼"}, - {":saudi_arabia:","🇸🇦"}, - {":solomon_islands:","🇸🇧"}, - {":seychelles:","🇸🇨"}, - {":sudan:","🇸🇩"}, - {":sweden:","🇸🇪"}, - {":singapore:","🇸🇬"}, - {":st_helena:","🇸🇭"}, - {":slovenia:","🇸🇮"}, - {":svalbard_jan_mayen:","🇸🇯"}, - {":slovakia:","🇸🇰"}, - {":sierra_leone:","🇸🇱"}, - {":san_marino:","🇸🇲"}, - {":senegal:","🇸🇳"}, - {":somalia:","🇸🇴"}, - {":suriname:","🇸🇷"}, - {":south_sudan:","🇸🇸"}, - {":sao_tome_principe:","🇸🇹"}, - {":el_salvador:","🇸🇻"}, - {":sint_maarten:","🇸🇽"}, - {":syria:","🇸🇾"}, - {":swaziland:","🇸🇿"}, - {":tristan_da_cunha:","🇹🇦"}, - {":turks_caicos_islands:","🇹🇨"}, - {":chad:","🇹🇩"}, - {":french_southern_territories:","🇹🇫"}, - {":togo:","🇹🇬"}, - {":thailand:","🇹🇭"}, - {":tajikistan:","🇹🇯"}, - {":tokelau:","🇹🇰"}, - {":timor_leste:","🇹🇱"}, - {":turkmenistan:","🇹🇲"}, - {":tunisia:","🇹🇳"}, - {":tonga:","🇹🇴"}, - {":tr:","🇹🇷"}, - {":trinidad_tobago:","🇹🇹"}, - {":tuvalu:","🇹🇻"}, - {":taiwan:","🇹🇼"}, - {":tanzania:","🇹🇿"}, - {":ukraine:","🇺🇦"}, - {":uganda:","🇺🇬"}, - {":us_outlying_islands:","🇺🇲"}, - {":united_nations:","🇺🇳"}, - {":uruguay:","🇺🇾"}, - {":uzbekistan:","🇺🇿"}, - {":vatican_city:","🇻🇦"}, - {":st_vincent_grenadines:","🇻🇨"}, - {":venezuela:","🇻🇪"}, - {":british_virgin_islands:","🇻🇬"}, - {":us_virgin_islands:","🇻🇮"}, - {":vietnam:","🇻🇳"}, - {":vanuatu:","🇻🇺"}, - {":wallis_futuna:","🇼🇫"}, - {":samoa:","🇼🇸"}, - {":kosovo:","🇽🇰"}, - {":yemen:","🇾🇪"}, - {":mayotte:","🇾🇹"}, - {":south_africa:","🇿🇦"}, - {":zambia:","🇿🇲"}, - {":zimbabwe:","🇿🇼"} - }; - } + { ":custom_arrow_left_strong:", "⇐"}, + { ":custom_arrow_right_strong:", "⇒"}, + { ":custom_arrow_left_right_strong:", "⇔"}, + {":rofl:","🤣"}, + {":slightly_smiling_face:","🙂"}, + {":upside_down_face:","🙃"}, + {":star_struck:","🤩"}, + {":zany_face:","🤪"}, + {":money_mouth_face:","🤑"}, + {":hugs:","🤗"}, + {":hand_over_mouth:","🤭"}, + {":shushing_face:","🤫"}, + {":thinking:","🤔"}, + {":zipper_mouth_face:","🤐"}, + {":raised_eyebrow:","🤨"}, + {":roll_eyes:","🙄"}, + {":lying_face:","🤥"}, + {":drooling_face:","🤤"}, + {":face_with_thermometer:","🤒"}, + {":face_with_head_bandage:","🤕"}, + {":nauseated_face:","🤢"}, + {":vomiting_face:","🤮"}, + {":sneezing_face:","🤧"}, + {":exploding_head:","🤯"}, + {":cowboy_hat_face:","🤠"}, + {":nerd_face:","🤓"}, + {":monocle_face:","🧐"}, + {":slightly_frowning_face:","🙁"}, + {":frowning_face:","☹️"}, + {":cursing_face:","🤬"}, + {":skull_and_crossbones:","☠️"}, + {":clown_face:","🤡"}, + {":robot:","🤖"}, + {":heavy_heart_exclamation:","❣️"}, + {":orange_heart:","🧡"}, + {":black_heart:","🖤"}, + {":hole:","🕳️"}, + {":eye_speech_bubble:","👁️‍🗨️"}, + {":left_speech_bubble:","🗨️"}, + {":right_anger_bubble:","🗯️"}, + {":raised_back_of_hand:","🤚"}, + {":raised_hand_with_fingers_splayed:","🖐️"}, + {":vulcan_salute:","🖖"}, + {":crossed_fingers:","🤞"}, + {":love_you_gesture:","🤟"}, + {":metal:","🤘"}, + {":call_me_hand:","🤙"}, + {":middle_finger:","🖕"}, + {":fist_raised:","✊"}, + {":fist_oncoming:","👊"}, + {":fist_left:","🤛"}, + {":fist_right:","🤜"}, + {":palms_up_together:","🤲"}, + {":handshake:","🤝"}, + {":writing_hand:","✍️"}, + {":selfie:","🤳"}, + {":brain:","🧠"}, + {":eye:","👁️"}, + {":child:","🧒"}, + {":adult:","🧑"}, + {":blond_haired_person:","👱"}, + {":bearded_person:","🧔"}, + {":blond_haired_man:","👱‍♂️"}, + {":blond_haired_woman:","👱‍♀️"}, + {":older_adult:","🧓"}, + {":frowning_person:","🙍"}, + {":frowning_man:","🙍‍♂️"}, + {":frowning_woman:","🙍‍♀️"}, + {":pouting_face:","🙎"}, + {":pouting_man:","🙎‍♂️"}, + {":pouting_woman:","🙎‍♀️"}, + {":no_good_man:","🙅‍♂️"}, + {":no_good_woman:","🙅‍♀️"}, + {":ok_person:","🙆"}, + {":ok_man:","🙆‍♂️"}, + {":tipping_hand_person:","💁"}, + {":tipping_hand_man:","💁‍♂️"}, + {":tipping_hand_woman:","💁‍♀️"}, + {":raising_hand_man:","🙋‍♂️"}, + {":raising_hand_woman:","🙋‍♀️"}, + {":bowing_man:","🙇‍♂️"}, + {":bowing_woman:","🙇‍♀️"}, + {":facepalm:","🤦"}, + {":man_facepalming:","🤦‍♂️"}, + {":woman_facepalming:","🤦‍♀️"}, + {":shrug:","🤷"}, + {":man_shrugging:","🤷‍♂️"}, + {":woman_shrugging:","🤷‍♀️"}, + {":man_health_worker:","👨‍⚕️"}, + {":woman_health_worker:","👩‍⚕️"}, + {":man_student:","👨‍🎓"}, + {":woman_student:","👩‍🎓"}, + {":man_teacher:","👨‍🏫"}, + {":woman_teacher:","👩‍🏫"}, + {":man_judge:","👨‍⚖️"}, + {":woman_judge:","👩‍⚖️"}, + {":man_farmer:","👨‍🌾"}, + {":woman_farmer:","👩‍🌾"}, + {":man_cook:","👨‍🍳"}, + {":woman_cook:","👩‍🍳"}, + {":man_mechanic:","👨‍🔧"}, + {":woman_mechanic:","👩‍🔧"}, + {":man_factory_worker:","👨‍🏭"}, + {":woman_factory_worker:","👩‍🏭"}, + {":man_office_worker:","👨‍💼"}, + {":woman_office_worker:","👩‍💼"}, + {":man_scientist:","👨‍🔬"}, + {":woman_scientist:","👩‍🔬"}, + {":man_technologist:","👨‍💻"}, + {":woman_technologist:","👩‍💻"}, + {":man_singer:","👨‍🎤"}, + {":woman_singer:","👩‍🎤"}, + {":man_artist:","👨‍🎨"}, + {":woman_artist:","👩‍🎨"}, + {":man_pilot:","👨‍✈️"}, + {":woman_pilot:","👩‍✈️"}, + {":man_astronaut:","👨‍🚀"}, + {":woman_astronaut:","👩‍🚀"}, + {":man_firefighter:","👨‍🚒"}, + {":woman_firefighter:","👩‍🚒"}, + {":police_officer:","👮"}, + {":policeman:","👮‍♂️"}, + {":policewoman:","👮‍♀️"}, + {":detective:","🕵️"}, + {":male_detective:","🕵️‍♂️"}, + {":female_detective:","🕵️‍♀️"}, + {":guard:","💂"}, + {":guardswoman:","💂‍♀️"}, + {":construction_worker_man:","👷‍♂️"}, + {":construction_worker_woman:","👷‍♀️"}, + {":prince:","🤴"}, + {":person_with_turban:","👳"}, + {":woman_with_turban:","👳‍♀️"}, + {":woman_with_headscarf:","🧕"}, + {":man_in_tuxedo:","🤵"}, + {":pregnant_woman:","🤰"}, + {":breast_feeding:","🤱"}, + {":mrs_claus:","🤶"}, + {":mage:","🧙"}, + {":mage_man:","🧙‍♂️"}, + {":mage_woman:","🧙‍♀️"}, + {":fairy:","🧚"}, + {":fairy_man:","🧚‍♂️"}, + {":fairy_woman:","🧚‍♀️"}, + {":vampire:","🧛"}, + {":vampire_man:","🧛‍♂️"}, + {":vampire_woman:","🧛‍♀️"}, + {":merperson:","🧜"}, + {":merman:","🧜‍♂️"}, + {":mermaid:","🧜‍♀️"}, + {":elf:","🧝"}, + {":elf_man:","🧝‍♂️"}, + {":elf_woman:","🧝‍♀️"}, + {":genie:","🧞"}, + {":genie_man:","🧞‍♂️"}, + {":genie_woman:","🧞‍♀️"}, + {":zombie:","🧟"}, + {":zombie_man:","🧟‍♂️"}, + {":zombie_woman:","🧟‍♀️"}, + {":massage_man:","💆‍♂️"}, + {":massage_woman:","💆‍♀️"}, + {":haircut_man:","💇‍♂️"}, + {":haircut_woman:","💇‍♀️"}, + {":walking_man:","🚶‍♂️"}, + {":walking_woman:","🚶‍♀️"}, + {":running_man:","🏃‍♂️"}, + {":running_woman:","🏃‍♀️"}, + {":woman_dancing:","💃"}, + {":man_dancing:","🕺"}, + {":business_suit_levitating:","🕴️"}, + {":dancing_men:","👯‍♂️"}, + {":dancing_women:","👯‍♀️"}, + {":sauna_person:","🧖"}, + {":sauna_man:","🧖‍♂️"}, + {":sauna_woman:","🧖‍♀️"}, + {":climbing:","🧗"}, + {":climbing_man:","🧗‍♂️"}, + {":climbing_woman:","🧗‍♀️"}, + {":person_fencing:","🤺"}, + {":skier:","⛷️"}, + {":golfing:","🏌️"}, + {":golfing_man:","🏌️‍♂️"}, + {":golfing_woman:","🏌️‍♀️"}, + {":surfing_man:","🏄‍♂️"}, + {":surfing_woman:","🏄‍♀️"}, + {":rowing_man:","🚣‍♂️"}, + {":rowing_woman:","🚣‍♀️"}, + {":swimming_man:","🏊‍♂️"}, + {":swimming_woman:","🏊‍♀️"}, + {":bouncing_ball_person:","⛹️"}, + {":bouncing_ball_man:","⛹️‍♂️"}, + {":bouncing_ball_woman:","⛹️‍♀️"}, + {":weight_lifting:","🏋️"}, + {":weight_lifting_man:","🏋️‍♂️"}, + {":weight_lifting_woman:","🏋️‍♀️"}, + {":biking_man:","🚴‍♂️"}, + {":biking_woman:","🚴‍♀️"}, + {":mountain_biking_man:","🚵‍♂️"}, + {":mountain_biking_woman:","🚵‍♀️"}, + {":cartwheeling:","🤸"}, + {":man_cartwheeling:","🤸‍♂️"}, + {":woman_cartwheeling:","🤸‍♀️"}, + {":wrestling:","🤼"}, + {":men_wrestling:","🤼‍♂️"}, + {":women_wrestling:","🤼‍♀️"}, + {":water_polo:","🤽"}, + {":man_playing_water_polo:","🤽‍♂️"}, + {":woman_playing_water_polo:","🤽‍♀️"}, + {":handball_person:","🤾"}, + {":man_playing_handball:","🤾‍♂️"}, + {":woman_playing_handball:","🤾‍♀️"}, + {":juggling_person:","🤹"}, + {":man_juggling:","🤹‍♂️"}, + {":woman_juggling:","🤹‍♀️"}, + {":lotus_position:","🧘"}, + {":lotus_position_man:","🧘‍♂️"}, + {":lotus_position_woman:","🧘‍♀️"}, + {":sleeping_bed:","🛌"}, + {":people_holding_hands:","🧑‍🤝‍🧑"}, + {":couplekiss_man_woman:","👩‍❤️‍💋‍👨"}, + {":couplekiss_man_man:","👨‍❤️‍💋‍👨"}, + {":couplekiss_woman_woman:","👩‍❤️‍💋‍👩"}, + {":couple_with_heart_woman_man:","👩‍❤️‍👨"}, + {":couple_with_heart_man_man:","👨‍❤️‍👨"}, + {":couple_with_heart_woman_woman:","👩‍❤️‍👩"}, + {":family_man_woman_boy:","👨‍👩‍👦"}, + {":family_man_woman_girl:","👨‍👩‍👧"}, + {":family_man_woman_girl_boy:","👨‍👩‍👧‍👦"}, + {":family_man_woman_boy_boy:","👨‍👩‍👦‍👦"}, + {":family_man_woman_girl_girl:","👨‍👩‍👧‍👧"}, + {":family_man_man_boy:","👨‍👨‍👦"}, + {":family_man_man_girl:","👨‍👨‍👧"}, + {":family_man_man_girl_boy:","👨‍👨‍👧‍👦"}, + {":family_man_man_boy_boy:","👨‍👨‍👦‍👦"}, + {":family_man_man_girl_girl:","👨‍👨‍👧‍👧"}, + {":family_woman_woman_boy:","👩‍👩‍👦"}, + {":family_woman_woman_girl:","👩‍👩‍👧"}, + {":family_woman_woman_girl_boy:","👩‍👩‍👧‍👦"}, + {":family_woman_woman_boy_boy:","👩‍👩‍👦‍👦"}, + {":family_woman_woman_girl_girl:","👩‍👩‍👧‍👧"}, + {":family_man_boy:","👨‍👦"}, + {":family_man_boy_boy:","👨‍👦‍👦"}, + {":family_man_girl:","👨‍👧"}, + {":family_man_girl_boy:","👨‍👧‍👦"}, + {":family_man_girl_girl:","👨‍👧‍👧"}, + {":family_woman_boy:","👩‍👦"}, + {":family_woman_boy_boy:","👩‍👦‍👦"}, + {":family_woman_girl:","👩‍👧"}, + {":family_woman_girl_boy:","👩‍👧‍👦"}, + {":family_woman_girl_girl:","👩‍👧‍👧"}, + {":speaking_head:","🗣️"}, + {":gorilla:","🦍"}, + {":fox_face:","🦊"}, + {":lion:","🦁"}, + {":unicorn:","🦄"}, + {":zebra:","🦓"}, + {":deer:","🦌"}, + {":giraffe:","🦒"}, + {":rhinoceros:","🦏"}, + {":chipmunk:","🐿️"}, + {":hedgehog:","🦔"}, + {":bat:","🦇"}, + {":turkey:","🦃"}, + {":dove:","🕊️"}, + {":eagle:","🦅"}, + {":duck:","🦆"}, + {":owl:","🦉"}, + {":lizard:","🦎"}, + {":sauropod:","🦕"}, + {":t-rex:","🦖"}, + {":shark:","🦈"}, + {":butterfly:","🦋"}, + {":cricket:","🦗"}, + {":spider:","🕷️"}, + {":spider_web:","🕸️"}, + {":scorpion:","🦂"}, + {":rosette:","🏵️"}, + {":wilted_flower:","🥀"}, + {":shamrock:","☘️"}, + {":kiwi_fruit:","🥝"}, + {":coconut:","🥥"}, + {":avocado:","🥑"}, + {":potato:","🥔"}, + {":carrot:","🥕"}, + {":hot_pepper:","🌶️"}, + {":cucumber:","🥒"}, + {":broccoli:","🥦"}, + {":peanuts:","🥜"}, + {":croissant:","🥐"}, + {":baguette_bread:","🥖"}, + {":pretzel:","🥨"}, + {":pancakes:","🥞"}, + {":cheese:","🧀"}, + {":cut_of_meat:","🥩"}, + {":bacon:","🥓"}, + {":hotdog:","🌭"}, + {":sandwich:","🥪"}, + {":taco:","🌮"}, + {":burrito:","🌯"}, + {":stuffed_flatbread:","🥙"}, + {":fried_egg:","🍳"}, + {":shallow_pan_of_food:","🥘"}, + {":bowl_with_spoon:","🥣"}, + {":green_salad:","🥗"}, + {":popcorn:","🍿"}, + {":canned_food:","🥫"}, + {":dumpling:","🥟"}, + {":fortune_cookie:","🥠"}, + {":takeout_box:","🥡"}, + {":crab:","🦀"}, + {":shrimp:","🦐"}, + {":squid:","🦑"}, + {":pie:","🥧"}, + {":milk_glass:","🥛"}, + {":champagne:","🍾"}, + {":clinking_glasses:","🥂"}, + {":tumbler_glass:","🥃"}, + {":cup_with_straw:","🥤"}, + {":chopsticks:","🥢"}, + {":plate_with_cutlery:","🍽️"}, + {":spoon:","🥄"}, + {":amphora:","🏺"}, + {":world_map:","🗺️"}, + {":mountain_snow:","🏔️"}, + {":mountain:","⛰️"}, + {":camping:","🏕️"}, + {":beach_umbrella:","🏖️"}, + {":desert:","🏜️"}, + {":desert_island:","🏝️"}, + {":national_park:","🏞️"}, + {":stadium:","🏟️"}, + {":classical_building:","🏛️"}, + {":building_construction:","🏗️"}, + {":houses:","🏘️"}, + {":derelict_house:","🏚️"}, + {":mosque:","🕌"}, + {":synagogue:","🕍"}, + {":shinto_shrine:","⛩️"}, + {":kaaba:","🕋"}, + {":cityscape:","🏙️"}, + {":racing_car:","🏎️"}, + {":motorcycle:","🏍️"}, + {":motor_scooter:","🛵"}, + {":kick_scooter:","🛴"}, + {":motorway:","🛣️"}, + {":railway_track:","🛤️"}, + {":oil_drum:","🛢️"}, + {":stop_sign:","🛑"}, + {":canoe:","🛶"}, + {":passenger_ship:","🛳️"}, + {":ferry:","⛴️"}, + {":motor_boat:","🛥️"}, + {":small_airplane:","🛩️"}, + {":flight_departure:","🛫"}, + {":flight_arrival:","🛬"}, + {":artificial_satellite:","🛰️"}, + {":flying_saucer:","🛸"}, + {":bellhop_bell:","🛎️"}, + {":stopwatch:","⏱️"}, + {":timer_clock:","⏲️"}, + {":mantelpiece_clock:","🕰️"}, + {":thermometer:","🌡️"}, + {":cloud_with_lightning_and_rain:","⛈️"}, + {":sun_behind_small_cloud:","🌤️"}, + {":sun_behind_large_cloud:","🌥️"}, + {":sun_behind_rain_cloud:","🌦️"}, + {":cloud_with_rain:","🌧️"}, + {":cloud_with_snow:","🌨️"}, + {":cloud_with_lightning:","🌩️"}, + {":tornado:","🌪️"}, + {":fog:","🌫️"}, + {":wind_face:","🌬️"}, + {":open_umbrella:","☂️"}, + {":parasol_on_ground:","⛱️"}, + {":snowman_with_snow:","☃️"}, + {":comet:","☄️"}, + {":reminder_ribbon:","🎗️"}, + {":tickets:","🎟️"}, + {":medal_military:","🎖️"}, + {":medal_sports:","🏅"}, + {":1st_place_medal:","🥇"}, + {":2nd_place_medal:","🥈"}, + {":3rd_place_medal:","🥉"}, + {":volleyball:","🏐"}, + {":cricket_game:","🏏"}, + {":field_hockey:","🏑"}, + {":ice_hockey:","🏒"}, + {":ping_pong:","🏓"}, + {":badminton:","🏸"}, + {":boxing_glove:","🥊"}, + {":martial_arts_uniform:","🥋"}, + {":goal_net:","🥅"}, + {":ice_skate:","⛸️"}, + {":sled:","🛷"}, + {":curling_stone:","🥌"}, + {":joystick:","🕹️"}, + {":chess_pawn:","♟️"}, + {":framed_picture:","🖼️"}, + {":dark_sunglasses:","🕶️"}, + {":scarf:","🧣"}, + {":gloves:","🧤"}, + {":coat:","🧥"}, + {":socks:","🧦"}, + {":shopping:","🛍️"}, + {":billed_cap:","🧢"}, + {":rescue_worker_helmet:","⛑️"}, + {":prayer_beads:","📿"}, + {":studio_microphone:","🎙️"}, + {":level_slider:","🎚️"}, + {":control_knobs:","🎛️"}, + {":drum:","🥁"}, + {":desktop_computer:","🖥️"}, + {":printer:","🖨️"}, + {":keyboard:","⌨️"}, + {":computer_mouse:","🖱️"}, + {":trackball:","🖲️"}, + {":film_strip:","🎞️"}, + {":film_projector:","📽️"}, + {":camera_flash:","📸"}, + {":candle:","🕯️"}, + {":newspaper_roll:","🗞️"}, + {":label:","🏷️"}, + {":ballot_box:","🗳️"}, + {":fountain_pen:","🖋️"}, + {":pen:","🖊️"}, + {":paintbrush:","🖌️"}, + {":crayon:","🖍️"}, + {":card_index_dividers:","🗂️"}, + {":spiral_notepad:","🗒️"}, + {":spiral_calendar:","🗓️"}, + {":paperclips:","🖇️"}, + {":card_file_box:","🗃️"}, + {":file_cabinet:","🗄️"}, + {":wastebasket:","🗑️"}, + {":old_key:","🗝️"}, + {":pick:","⛏️"}, + {":hammer_and_pick:","⚒️"}, + {":hammer_and_wrench:","🛠️"}, + {":dagger:","🗡️"}, + {":crossed_swords:","⚔️"}, + {":bow_and_arrow:","🏹"}, + {":shield:","🛡️"}, + {":gear:","⚙️"}, + {":clamp:","🗜️"}, + {":balance_scale:","⚖️"}, + {":chains:","⛓️"}, + {":alembic:","⚗️"}, + {":bed:","🛏️"}, + {":couch_and_lamp:","🛋️"}, + {":shopping_cart:","🛒"}, + {":coffin:","⚰️"}, + {":funeral_urn:","⚱️"}, + {":radioactive:","☢️"}, + {":biohazard:","☣️"}, + {":place_of_worship:","🛐"}, + {":atom_symbol:","⚛️"}, + {":om:","🕉️"}, + {":star_of_david:","✡️"}, + {":wheel_of_dharma:","☸️"}, + {":yin_yang:","☯️"}, + {":latin_cross:","✝️"}, + {":orthodox_cross:","☦️"}, + {":star_and_crescent:","☪️"}, + {":peace_symbol:","☮️"}, + {":menorah:","🕎"}, + {":next_track_button:","⏭️"}, + {":play_or_pause_button:","⏯️"}, + {":previous_track_button:","⏮️"}, + {":pause_button:","⏸️"}, + {":stop_button:","⏹️"}, + {":record_button:","⏺️"}, + {":eject_button:","⏏️"}, + {":female_sign:","♀️"}, + {":male_sign:","♂️"}, + {":medical_symbol:","⚕️"}, + {":infinity:","♾️"}, + {":fleur_de_lis:","⚜️"}, + {":asterisk:","*️⃣"}, + {":black_flag:","🏴"}, + {":white_flag:","🏳️"}, + {":rainbow_flag:","🏳️‍🌈"}, + {":pirate_flag:","🏴‍☠️"}, + {":ascension_island:","🇦🇨"}, + {":andorra:","🇦🇩"}, + {":united_arab_emirates:","🇦🇪"}, + {":afghanistan:","🇦🇫"}, + {":antigua_barbuda:","🇦🇬"}, + {":anguilla:","🇦🇮"}, + {":albania:","🇦🇱"}, + {":armenia:","🇦🇲"}, + {":angola:","🇦🇴"}, + {":antarctica:","🇦🇶"}, + {":argentina:","🇦🇷"}, + {":american_samoa:","🇦🇸"}, + {":austria:","🇦🇹"}, + {":australia:","🇦🇺"}, + {":aruba:","🇦🇼"}, + {":aland_islands:","🇦🇽"}, + {":azerbaijan:","🇦🇿"}, + {":bosnia_herzegovina:","🇧🇦"}, + {":barbados:","🇧🇧"}, + {":bangladesh:","🇧🇩"}, + {":belgium:","🇧🇪"}, + {":burkina_faso:","🇧🇫"}, + {":bulgaria:","🇧🇬"}, + {":bahrain:","🇧🇭"}, + {":burundi:","🇧🇮"}, + {":benin:","🇧🇯"}, + {":st_barthelemy:","🇧🇱"}, + {":bermuda:","🇧🇲"}, + {":brunei:","🇧🇳"}, + {":bolivia:","🇧🇴"}, + {":caribbean_netherlands:","🇧🇶"}, + {":brazil:","🇧🇷"}, + {":bahamas:","🇧🇸"}, + {":bhutan:","🇧🇹"}, + {":bouvet_island:","🇧🇻"}, + {":botswana:","🇧🇼"}, + {":belarus:","🇧🇾"}, + {":belize:","🇧🇿"}, + {":canada:","🇨🇦"}, + {":cocos_islands:","🇨🇨"}, + {":congo_kinshasa:","🇨🇩"}, + {":central_african_republic:","🇨🇫"}, + {":congo_brazzaville:","🇨🇬"}, + {":switzerland:","🇨🇭"}, + {":cote_divoire:","🇨🇮"}, + {":cook_islands:","🇨🇰"}, + {":chile:","🇨🇱"}, + {":cameroon:","🇨🇲"}, + {":colombia:","🇨🇴"}, + {":clipperton_island:","🇨🇵"}, + {":costa_rica:","🇨🇷"}, + {":cuba:","🇨🇺"}, + {":cape_verde:","🇨🇻"}, + {":curacao:","🇨🇼"}, + {":christmas_island:","🇨🇽"}, + {":cyprus:","🇨🇾"}, + {":czech_republic:","🇨🇿"}, + {":diego_garcia:","🇩🇬"}, + {":djibouti:","🇩🇯"}, + {":denmark:","🇩🇰"}, + {":dominica:","🇩🇲"}, + {":dominican_republic:","🇩🇴"}, + {":algeria:","🇩🇿"}, + {":ceuta_melilla:","🇪🇦"}, + {":ecuador:","🇪🇨"}, + {":estonia:","🇪🇪"}, + {":egypt:","🇪🇬"}, + {":western_sahara:","🇪🇭"}, + {":eritrea:","🇪🇷"}, + {":ethiopia:","🇪🇹"}, + {":eu:","🇪🇺"}, + {":finland:","🇫🇮"}, + {":fiji:","🇫🇯"}, + {":falkland_islands:","🇫🇰"}, + {":micronesia:","🇫🇲"}, + {":faroe_islands:","🇫🇴"}, + {":gabon:","🇬🇦"}, + {":grenada:","🇬🇩"}, + {":georgia:","🇬🇪"}, + {":french_guiana:","🇬🇫"}, + {":guernsey:","🇬🇬"}, + {":ghana:","🇬🇭"}, + {":gibraltar:","🇬🇮"}, + {":greenland:","🇬🇱"}, + {":gambia:","🇬🇲"}, + {":guinea:","🇬🇳"}, + {":guadeloupe:","🇬🇵"}, + {":equatorial_guinea:","🇬🇶"}, + {":greece:","🇬🇷"}, + {":south_georgia_south_sandwich_islands:","🇬🇸"}, + {":guatemala:","🇬🇹"}, + {":guam:","🇬🇺"}, + {":guinea_bissau:","🇬🇼"}, + {":guyana:","🇬🇾"}, + {":hong_kong:","🇭🇰"}, + {":heard_mcdonald_islands:","🇭🇲"}, + {":honduras:","🇭🇳"}, + {":croatia:","🇭🇷"}, + {":haiti:","🇭🇹"}, + {":hungary:","🇭🇺"}, + {":canary_islands:","🇮🇨"}, + {":indonesia:","🇮🇩"}, + {":ireland:","🇮🇪"}, + {":israel:","🇮🇱"}, + {":isle_of_man:","🇮🇲"}, + {":india:","🇮🇳"}, + {":british_indian_ocean_territory:","🇮🇴"}, + {":iraq:","🇮🇶"}, + {":iran:","🇮🇷"}, + {":iceland:","🇮🇸"}, + {":jersey:","🇯🇪"}, + {":jamaica:","🇯🇲"}, + {":jordan:","🇯🇴"}, + {":kenya:","🇰🇪"}, + {":kyrgyzstan:","🇰🇬"}, + {":cambodia:","🇰🇭"}, + {":kiribati:","🇰🇮"}, + {":comoros:","🇰🇲"}, + {":st_kitts_nevis:","🇰🇳"}, + {":north_korea:","🇰🇵"}, + {":kuwait:","🇰🇼"}, + {":cayman_islands:","🇰🇾"}, + {":kazakhstan:","🇰🇿"}, + {":laos:","🇱🇦"}, + {":lebanon:","🇱🇧"}, + {":st_lucia:","🇱🇨"}, + {":liechtenstein:","🇱🇮"}, + {":sri_lanka:","🇱🇰"}, + {":liberia:","🇱🇷"}, + {":lesotho:","🇱🇸"}, + {":lithuania:","🇱🇹"}, + {":luxembourg:","🇱🇺"}, + {":latvia:","🇱🇻"}, + {":libya:","🇱🇾"}, + {":morocco:","🇲🇦"}, + {":monaco:","🇲🇨"}, + {":moldova:","🇲🇩"}, + {":montenegro:","🇲🇪"}, + {":st_martin:","🇲🇫"}, + {":madagascar:","🇲🇬"}, + {":marshall_islands:","🇲🇭"}, + {":macedonia:","🇲🇰"}, + {":mali:","🇲🇱"}, + {":myanmar:","🇲🇲"}, + {":mongolia:","🇲🇳"}, + {":macau:","🇲🇴"}, + {":northern_mariana_islands:","🇲🇵"}, + {":martinique:","🇲🇶"}, + {":mauritania:","🇲🇷"}, + {":montserrat:","🇲🇸"}, + {":malta:","🇲🇹"}, + {":mauritius:","🇲🇺"}, + {":maldives:","🇲🇻"}, + {":malawi:","🇲🇼"}, + {":mexico:","🇲🇽"}, + {":malaysia:","🇲🇾"}, + {":mozambique:","🇲🇿"}, + {":namibia:","🇳🇦"}, + {":new_caledonia:","🇳🇨"}, + {":niger:","🇳🇪"}, + {":norfolk_island:","🇳🇫"}, + {":nigeria:","🇳🇬"}, + {":nicaragua:","🇳🇮"}, + {":netherlands:","🇳🇱"}, + {":norway:","🇳🇴"}, + {":nepal:","🇳🇵"}, + {":nauru:","🇳🇷"}, + {":niue:","🇳🇺"}, + {":new_zealand:","🇳🇿"}, + {":oman:","🇴🇲"}, + {":panama:","🇵🇦"}, + {":peru:","🇵🇪"}, + {":french_polynesia:","🇵🇫"}, + {":papua_new_guinea:","🇵🇬"}, + {":philippines:","🇵🇭"}, + {":pakistan:","🇵🇰"}, + {":poland:","🇵🇱"}, + {":st_pierre_miquelon:","🇵🇲"}, + {":pitcairn_islands:","🇵🇳"}, + {":puerto_rico:","🇵🇷"}, + {":palestinian_territories:","🇵🇸"}, + {":portugal:","🇵🇹"}, + {":palau:","🇵🇼"}, + {":paraguay:","🇵🇾"}, + {":qatar:","🇶🇦"}, + {":reunion:","🇷🇪"}, + {":romania:","🇷🇴"}, + {":serbia:","🇷🇸"}, + {":rwanda:","🇷🇼"}, + {":saudi_arabia:","🇸🇦"}, + {":solomon_islands:","🇸🇧"}, + {":seychelles:","🇸🇨"}, + {":sudan:","🇸🇩"}, + {":sweden:","🇸🇪"}, + {":singapore:","🇸🇬"}, + {":st_helena:","🇸🇭"}, + {":slovenia:","🇸🇮"}, + {":svalbard_jan_mayen:","🇸🇯"}, + {":slovakia:","🇸🇰"}, + {":sierra_leone:","🇸🇱"}, + {":san_marino:","🇸🇲"}, + {":senegal:","🇸🇳"}, + {":somalia:","🇸🇴"}, + {":suriname:","🇸🇷"}, + {":south_sudan:","🇸🇸"}, + {":sao_tome_principe:","🇸🇹"}, + {":el_salvador:","🇸🇻"}, + {":sint_maarten:","🇸🇽"}, + {":syria:","🇸🇾"}, + {":swaziland:","🇸🇿"}, + {":tristan_da_cunha:","🇹🇦"}, + {":turks_caicos_islands:","🇹🇨"}, + {":chad:","🇹🇩"}, + {":french_southern_territories:","🇹🇫"}, + {":togo:","🇹🇬"}, + {":thailand:","🇹🇭"}, + {":tajikistan:","🇹🇯"}, + {":tokelau:","🇹🇰"}, + {":timor_leste:","🇹🇱"}, + {":turkmenistan:","🇹🇲"}, + {":tunisia:","🇹🇳"}, + {":tonga:","🇹🇴"}, + {":tr:","🇹🇷"}, + {":trinidad_tobago:","🇹🇹"}, + {":tuvalu:","🇹🇻"}, + {":taiwan:","🇹🇼"}, + {":tanzania:","🇹🇿"}, + {":ukraine:","🇺🇦"}, + {":uganda:","🇺🇬"}, + {":us_outlying_islands:","🇺🇲"}, + {":united_nations:","🇺🇳"}, + {":uruguay:","🇺🇾"}, + {":uzbekistan:","🇺🇿"}, + {":vatican_city:","🇻🇦"}, + {":st_vincent_grenadines:","🇻🇨"}, + {":venezuela:","🇻🇪"}, + {":british_virgin_islands:","🇻🇬"}, + {":us_virgin_islands:","🇻🇮"}, + {":vietnam:","🇻🇳"}, + {":vanuatu:","🇻🇺"}, + {":wallis_futuna:","🇼🇫"}, + {":samoa:","🇼🇸"}, + {":kosovo:","🇽🇰"}, + {":yemen:","🇾🇪"}, + {":mayotte:","🇾🇹"}, + {":south_africa:","🇿🇦"}, + {":zambia:","🇿🇲"}, + {":zimbabwe:","🇿🇼"} + }; + } - /// - /// Gets a new instance of the default smiley to emoji shortcode dictionary. - /// It can be used to create a customized . - /// - public static IDictionary GetDefaultSmileyToEmojiShortcode() + /// + /// Gets a new instance of the default smiley to emoji shortcode dictionary. + /// It can be used to create a customized . + /// + public static IDictionary GetDefaultSmileyToEmojiShortcode() + { + return new Dictionary(71) { - return new Dictionary(71) - { - {">:(", ":angry:"}, - {">:-(", ":angry:"}, - {":\")", ":blush:"}, - {":-\")", ":blush:"}, - {":(", ":angry:"}, + {">:-(", ":angry:"}, + {":\")", ":blush:"}, + {":-\")", ":blush:"}, + {"", ":custom_arrow_right:" }, - {"<->", ":custom_arrow_left_right:" }, + // Custom arrows + {"<-", ":custom_arrow_left:" }, + {"->", ":custom_arrow_right:" }, + {"<->", ":custom_arrow_left_right:" }, - {"<=", ":custom_arrow_left_strong:" }, - {"=>", ":custom_arrow_right_strong:" }, - {"<=>", ":custom_arrow_left_right_strong:" }, - }; - } - - #endregion + {"<=", ":custom_arrow_left_strong:" }, + {"=>", ":custom_arrow_right_strong:" }, + {"<=>", ":custom_arrow_left_right_strong:" }, + }; + } - /// - /// Constructs a mapping for the default emoji shortcodes and smileys. - /// - public EmojiMapping(bool enableSmileys = true) - : this(GetDefaultEmojiShortcodeToUnicode(), - enableSmileys ? GetDefaultSmileyToEmojiShortcode() : new Dictionary()) - { } + #endregion - /// - /// Constructs a mapping from a dictionary of emoji shortcodes to unicode, and a dictionary of smileys to emoji shortcodes. - /// - public EmojiMapping(IDictionary shortcodeToUnicode, IDictionary smileyToShortcode) - { - if (shortcodeToUnicode is null) - ThrowHelper.ArgumentNullException(nameof(shortcodeToUnicode)); + /// + /// Constructs a mapping for the default emoji shortcodes and smileys. + /// + public EmojiMapping(bool enableSmileys = true) + : this(GetDefaultEmojiShortcodeToUnicode(), + enableSmileys ? GetDefaultSmileyToEmojiShortcode() : new Dictionary()) + { } - if (smileyToShortcode is null) - ThrowHelper.ArgumentNullException(nameof(smileyToShortcode)); + /// + /// Constructs a mapping from a dictionary of emoji shortcodes to unicode, and a dictionary of smileys to emoji shortcodes. + /// + public EmojiMapping(IDictionary shortcodeToUnicode, IDictionary smileyToShortcode) + { + if (shortcodeToUnicode is null) + ThrowHelper.ArgumentNullException(nameof(shortcodeToUnicode)); - // Build emojis and smileys CompactPrefixTree + if (smileyToShortcode is null) + ThrowHelper.ArgumentNullException(nameof(smileyToShortcode)); - int jointCount = shortcodeToUnicode.Count + smileyToShortcode.Count; + // Build emojis and smileys CompactPrefixTree - // Count * 2 seems to be a good fit for the data set - PrefixTree = new CompactPrefixTree(jointCount, jointCount * 2, jointCount * 2); + int jointCount = shortcodeToUnicode.Count + smileyToShortcode.Count; - // This is not the best data set for the prefix tree as it will have to check the first character linearly - // A work-around would require a bunch of substrings / removing the leading ':' from emojis, neither one is pretty - // This way we sacrifice a few microseconds for not introducing breaking changes, emojis aren't all that common anyhow + // Count * 2 seems to be a good fit for the data set + PrefixTree = new CompactPrefixTree(jointCount, jointCount * 2, jointCount * 2); - var firstChars = new HashSet(); + // This is not the best data set for the prefix tree as it will have to check the first character linearly + // A work-around would require a bunch of substrings / removing the leading ':' from emojis, neither one is pretty + // This way we sacrifice a few microseconds for not introducing breaking changes, emojis aren't all that common anyhow - foreach (var shortcode in shortcodeToUnicode) - { - if (string.IsNullOrEmpty(shortcode.Key) || string.IsNullOrEmpty(shortcode.Value)) - ThrowHelper.ArgumentException("The dictionaries cannot contain null or empty keys/values", nameof(shortcodeToUnicode)); + var firstChars = new HashSet(); - firstChars.Add(shortcode.Key[0]); - PrefixTree.Add(shortcode); - } + foreach (var shortcode in shortcodeToUnicode) + { + if (string.IsNullOrEmpty(shortcode.Key) || string.IsNullOrEmpty(shortcode.Value)) + ThrowHelper.ArgumentException("The dictionaries cannot contain null or empty keys/values", nameof(shortcodeToUnicode)); - foreach (var smiley in smileyToShortcode) - { - if (string.IsNullOrEmpty(smiley.Key) || string.IsNullOrEmpty(smiley.Value)) - ThrowHelper.ArgumentException("The dictionaries cannot contain null or empty keys/values", nameof(smileyToShortcode)); + firstChars.Add(shortcode.Key[0]); + PrefixTree.Add(shortcode); + } - if (!shortcodeToUnicode.TryGetValue(smiley.Value, out string? unicode)) - ThrowHelper.ArgumentException(string.Format("Invalid smiley target: {0} is not present in the emoji shortcodes dictionary", smiley.Value)); + foreach (var smiley in smileyToShortcode) + { + if (string.IsNullOrEmpty(smiley.Key) || string.IsNullOrEmpty(smiley.Value)) + ThrowHelper.ArgumentException("The dictionaries cannot contain null or empty keys/values", nameof(smileyToShortcode)); - firstChars.Add(smiley.Key[0]); + if (!shortcodeToUnicode.TryGetValue(smiley.Value, out string? unicode)) + ThrowHelper.ArgumentException(string.Format("Invalid smiley target: {0} is not present in the emoji shortcodes dictionary", smiley.Value)); - if (!PrefixTree.TryAdd(smiley.Key, unicode)) - ThrowHelper.ArgumentException(string.Format("Smiley {0} is already present in the emoji mapping", smiley.Key)); - } + firstChars.Add(smiley.Key[0]); - OpeningCharacters = new List(firstChars).ToArray(); + if (!PrefixTree.TryAdd(smiley.Key, unicode)) + ThrowHelper.ArgumentException(string.Format("Smiley {0} is already present in the emoji mapping", smiley.Key)); } + + OpeningCharacters = new List(firstChars).ToArray(); } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Emoji/EmojiParser.cs b/src/Markdig/Extensions/Emoji/EmojiParser.cs index b325791bb..cda5c7c3f 100644 --- a/src/Markdig/Extensions/Emoji/EmojiParser.cs +++ b/src/Markdig/Extensions/Emoji/EmojiParser.cs @@ -2,61 +2,58 @@ // 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.Collections.Generic; using Markdig.Helpers; using Markdig.Parsers; -namespace Markdig.Extensions.Emoji +namespace Markdig.Extensions.Emoji; + +/// +/// The inline parser used for emojis. +/// +/// +public class EmojiParser : InlineParser { + private readonly EmojiMapping _emojiMapping; + /// - /// The inline parser used for emojis. + /// Initializes a new instance of the class. /// - /// - public class EmojiParser : InlineParser + public EmojiParser(EmojiMapping emojiMapping) { - private readonly EmojiMapping _emojiMapping; + _emojiMapping = emojiMapping; + OpeningCharacters = _emojiMapping.OpeningCharacters; + } - /// - /// Initializes a new instance of the class. - /// - public EmojiParser(EmojiMapping emojiMapping) + public override bool Match(InlineProcessor processor, ref StringSlice slice) + { + // Previous char must be a space + if (!slice.PeekCharExtra(-1).IsWhiteSpaceOrZero()) { - _emojiMapping = emojiMapping; - OpeningCharacters = _emojiMapping.OpeningCharacters; + return false; } - public override bool Match(InlineProcessor processor, ref StringSlice slice) + // Try to match an emoji shortcode or smiley + if (!_emojiMapping.PrefixTree.TryMatchLongest(slice.Text.AsSpan(slice.Start, slice.Length), out KeyValuePair match)) { - // Previous char must be a space - if (!slice.PeekCharExtra(-1).IsWhiteSpaceOrZero()) - { - return false; - } - - // Try to match an emoji shortcode or smiley - if (!_emojiMapping.PrefixTree.TryMatchLongest(slice.Text.AsSpan(slice.Start, slice.Length), out KeyValuePair match)) - { - return false; - } + return false; + } - // Push the EmojiInline - processor.Inline = new EmojiInline(match.Value) + // Push the EmojiInline + processor.Inline = new EmojiInline(match.Value) + { + Span = { - Span = - { - Start = processor.GetSourcePosition(slice.Start, out int line, out int column), - }, - Line = line, - Column = column, - Match = match.Key - }; - processor.Inline.Span.End = processor.Inline.Span.Start + match.Key.Length - 1; - - // Move the cursor to the character after the matched string - slice.Start += match.Key.Length; - - return true; - } + Start = processor.GetSourcePosition(slice.Start, out int line, out int column), + }, + Line = line, + Column = column, + Match = match.Key + }; + processor.Inline.Span.End = processor.Inline.Span.Start + match.Key.Length - 1; + + // Move the cursor to the character after the matched string + slice.Start += match.Key.Length; + + return true; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/EmphasisExtras/EmphasisExtraExtension.cs b/src/Markdig/Extensions/EmphasisExtras/EmphasisExtraExtension.cs index e671c6fe1..2b4557728 100644 --- a/src/Markdig/Extensions/EmphasisExtras/EmphasisExtraExtension.cs +++ b/src/Markdig/Extensions/EmphasisExtras/EmphasisExtraExtension.cs @@ -8,117 +8,116 @@ using Markdig.Syntax.Inlines; using System.Diagnostics; -namespace Markdig.Extensions.EmphasisExtras +namespace Markdig.Extensions.EmphasisExtras; + +/// +/// Extension for strikethrough, subscript, superscript, inserted and marked. +/// +/// +public class EmphasisExtraExtension : IMarkdownExtension { /// - /// Extension for strikethrough, subscript, superscript, inserted and marked. + /// Initializes a new instance of the class. /// - /// - public class EmphasisExtraExtension : IMarkdownExtension + /// The options. + public EmphasisExtraExtension(EmphasisExtraOptions options = EmphasisExtraOptions.Default) { - /// - /// Initializes a new instance of the class. - /// - /// The options. - public EmphasisExtraExtension(EmphasisExtraOptions options = EmphasisExtraOptions.Default) - { - Options = options; - } + Options = options; + } - /// - /// Gets the options. - /// - public EmphasisExtraOptions Options { get; } + /// + /// Gets the options. + /// + public EmphasisExtraOptions Options { get; } - public void Setup(MarkdownPipelineBuilder pipeline) + public void Setup(MarkdownPipelineBuilder pipeline) + { + var parser = pipeline.InlineParsers.FindExact(); + if (parser != null) { - var parser = pipeline.InlineParsers.FindExact(); - if (parser != null) - { - var hasTilde = false; - var hasSup = false; - var hasPlus = false; - var hasEqual = false; + var hasTilde = false; + var hasSup = false; + var hasPlus = false; + var hasEqual = false; - var requireTilde = ((Options & EmphasisExtraOptions.Strikethrough) != 0 || - (Options & EmphasisExtraOptions.Subscript) != 0); + var requireTilde = ((Options & EmphasisExtraOptions.Strikethrough) != 0 || + (Options & EmphasisExtraOptions.Subscript) != 0); - var requireSup = (Options & EmphasisExtraOptions.Superscript) != 0; - var requirePlus = (Options & EmphasisExtraOptions.Inserted) != 0; - var requireEqual = (Options & EmphasisExtraOptions.Marked) != 0; + var requireSup = (Options & EmphasisExtraOptions.Superscript) != 0; + var requirePlus = (Options & EmphasisExtraOptions.Inserted) != 0; + var requireEqual = (Options & EmphasisExtraOptions.Marked) != 0; - foreach (var emphasis in parser.EmphasisDescriptors) - { - if (requireTilde && emphasis.Character == '~') - { - hasTilde = true; - } - if (requireSup && emphasis.Character == '^') - { - hasSup = true; - } - if (requirePlus && emphasis.Character == '+') - { - hasPlus = true; - } - if (requireEqual && emphasis.Character == '=') - { - hasEqual = true; - } - } - - if (requireTilde && !hasTilde) + foreach (var emphasis in parser.EmphasisDescriptors) + { + if (requireTilde && emphasis.Character == '~') { - int minimumCount = (Options & EmphasisExtraOptions.Subscript) != 0 ? 1 : 2; - int maximumCount = (Options & EmphasisExtraOptions.Strikethrough) != 0 ? 2 : 1; - parser.EmphasisDescriptors.Add(new EmphasisDescriptor('~', minimumCount, maximumCount, true)); + hasTilde = true; } - if (requireSup && !hasSup) + if (requireSup && emphasis.Character == '^') { - parser.EmphasisDescriptors.Add(new EmphasisDescriptor('^', 1, 1, true)); + hasSup = true; } - if (requirePlus && !hasPlus) + if (requirePlus && emphasis.Character == '+') { - parser.EmphasisDescriptors.Add(new EmphasisDescriptor('+', 2, 2, true)); + hasPlus = true; } - if (requireEqual && !hasEqual) + if (requireEqual && emphasis.Character == '=') { - parser.EmphasisDescriptors.Add(new EmphasisDescriptor('=', 2, 2, true)); + hasEqual = true; } } - } - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) - { - if (renderer is HtmlRenderer htmlRenderer) + if (requireTilde && !hasTilde) { - // Extend the rendering here. - var emphasisRenderer = htmlRenderer.ObjectRenderers.FindExact(); - if (emphasisRenderer != null) - { - var previousTag = emphasisRenderer.GetTag; - emphasisRenderer.GetTag = inline => GetTag(inline) ?? previousTag(inline); - } + int minimumCount = (Options & EmphasisExtraOptions.Subscript) != 0 ? 1 : 2; + int maximumCount = (Options & EmphasisExtraOptions.Strikethrough) != 0 ? 2 : 1; + parser.EmphasisDescriptors.Add(new EmphasisDescriptor('~', minimumCount, maximumCount, true)); + } + if (requireSup && !hasSup) + { + parser.EmphasisDescriptors.Add(new EmphasisDescriptor('^', 1, 1, true)); + } + if (requirePlus && !hasPlus) + { + parser.EmphasisDescriptors.Add(new EmphasisDescriptor('+', 2, 2, true)); + } + if (requireEqual && !hasEqual) + { + parser.EmphasisDescriptors.Add(new EmphasisDescriptor('=', 2, 2, true)); } } + } - private string? GetTag(EmphasisInline emphasisInline) + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + if (renderer is HtmlRenderer htmlRenderer) { - var c = emphasisInline.DelimiterChar; - switch (c) + // Extend the rendering here. + var emphasisRenderer = htmlRenderer.ObjectRenderers.FindExact(); + if (emphasisRenderer != null) { - case '~': - Debug.Assert(emphasisInline.DelimiterCount <= 2); - return emphasisInline.DelimiterCount == 2 ? "del" : "sub"; - case '^': - return "sup"; - case '+': - return "ins"; - case '=': - return "mark"; + var previousTag = emphasisRenderer.GetTag; + emphasisRenderer.GetTag = inline => GetTag(inline) ?? previousTag(inline); } + } + } - return null; + private string? GetTag(EmphasisInline emphasisInline) + { + var c = emphasisInline.DelimiterChar; + switch (c) + { + case '~': + Debug.Assert(emphasisInline.DelimiterCount <= 2); + return emphasisInline.DelimiterCount == 2 ? "del" : "sub"; + case '^': + return "sup"; + case '+': + return "ins"; + case '=': + return "mark"; } + + return null; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/EmphasisExtras/EmphasisExtraOptions.cs b/src/Markdig/Extensions/EmphasisExtras/EmphasisExtraOptions.cs index bb0958f1e..0f28f3ddd 100644 --- a/src/Markdig/Extensions/EmphasisExtras/EmphasisExtraOptions.cs +++ b/src/Markdig/Extensions/EmphasisExtras/EmphasisExtraOptions.cs @@ -1,45 +1,42 @@ -// Copyright (c) Alexandre Mutel. All rights reserved. +// 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; +namespace Markdig.Extensions.EmphasisExtras; -namespace Markdig.Extensions.EmphasisExtras +/// +/// Options for enabling support for extra emphasis. +/// +[Flags] +public enum EmphasisExtraOptions { /// - /// Options for enabling support for extra emphasis. + /// Allows all extra emphasis (default). /// - [Flags] - public enum EmphasisExtraOptions - { - /// - /// Allows all extra emphasis (default). - /// - Default = Strikethrough | Subscript | Superscript | Inserted | Marked, + Default = Strikethrough | Subscript | Superscript | Inserted | Marked, - /// - /// A text that can be strikethrough using the double character ~~ - /// - Strikethrough = 1, + /// + /// A text that can be strikethrough using the double character ~~ + /// + Strikethrough = 1, - /// - /// A text that can be rendered as a subscript using the character ~ - /// - Subscript = 2, + /// + /// A text that can be rendered as a subscript using the character ~ + /// + Subscript = 2, - /// - /// A text that can be rendered as a superscript using the character ^ - /// - Superscript = 4, + /// + /// A text that can be rendered as a superscript using the character ^ + /// + Superscript = 4, - /// - /// A text that can be rendered as inserted using the double character ++ - /// - Inserted = 8, + /// + /// A text that can be rendered as inserted using the double character ++ + /// + Inserted = 8, - /// - /// A text that can be rendered as marked using the double character == - /// - Marked = 16, - } + /// + /// A text that can be rendered as marked using the double character == + /// + Marked = 16, } \ No newline at end of file diff --git a/src/Markdig/Extensions/Figures/Figure.cs b/src/Markdig/Extensions/Figures/Figure.cs index ca3f3d519..b688718cc 100644 --- a/src/Markdig/Extensions/Figures/Figure.cs +++ b/src/Markdig/Extensions/Figures/Figure.cs @@ -5,30 +5,29 @@ using Markdig.Parsers; using Markdig.Syntax; -namespace Markdig.Extensions.Figures +namespace Markdig.Extensions.Figures; + +/// +/// Defines a figure container. +/// +/// +public class Figure : ContainerBlock { /// - /// Defines a figure container. + /// Initializes a new instance of the class. /// - /// - public class Figure : ContainerBlock + /// The parser used to create this block. + public Figure(BlockParser parser) : base(parser) { - /// - /// Initializes a new instance of the class. - /// - /// The parser used to create this block. - public Figure(BlockParser parser) : base(parser) - { - } + } - /// - /// Gets or sets the opening character count used to open this figure code block. - /// - public int OpeningCharacterCount { get; set; } + /// + /// Gets or sets the opening character count used to open this figure code block. + /// + public int OpeningCharacterCount { get; set; } - /// - /// Gets or sets the opening character used to open and close this figure code block. - /// - public char OpeningCharacter { get; set; } - } + /// + /// Gets or sets the opening character used to open and close this figure code block. + /// + public char OpeningCharacter { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Figures/FigureBlockParser.cs b/src/Markdig/Extensions/Figures/FigureBlockParser.cs index ca715dcc0..1ad28bed4 100644 --- a/src/Markdig/Extensions/Figures/FigureBlockParser.cs +++ b/src/Markdig/Extensions/Figures/FigureBlockParser.cs @@ -5,52 +5,87 @@ using Markdig.Parsers; using Markdig.Syntax; -namespace Markdig.Extensions.Figures +namespace Markdig.Extensions.Figures; + +/// +/// The block parser for a block. +/// +/// +public class FigureBlockParser : BlockParser { /// - /// The block parser for a block. + /// Initializes a new instance of the class. /// - /// - public class FigureBlockParser : BlockParser + public FigureBlockParser() { - /// - /// Initializes a new instance of the class. - /// - public FigureBlockParser() - { - OpeningCharacters = new[] { '^' }; - } + OpeningCharacters = new[] { '^' }; + } - public override BlockState TryOpen(BlockProcessor processor) + public override BlockState TryOpen(BlockProcessor processor) + { + // We expect no indentation for a figure block. + if (processor.IsCodeIndent) { - // We expect no indentation for a figure block. - if (processor.IsCodeIndent) - { - return BlockState.None; - } + return BlockState.None; + } - // Match fenced char - var line = processor.Line; - char openingChar = line.CurrentChar; - int count = line.CountAndSkipChar(openingChar); + // Match fenced char + var line = processor.Line; + char openingChar = line.CurrentChar; + int count = line.CountAndSkipChar(openingChar); - // Requires at least 3 opening chars - if (count < 3) - { - return BlockState.None; - } + // Requires at least 3 opening chars + if (count < 3) + { + return BlockState.None; + } - int startPosition = processor.Start; - int column = processor.Column; - var figure = new Figure(this) + int startPosition = processor.Start; + int column = processor.Column; + var figure = new Figure(this) + { + Span = new SourceSpan(startPosition, line.End), + Line = processor.LineIndex, + Column = column, + OpeningCharacter = openingChar, + OpeningCharacterCount = count + }; + + line.TrimStart(); + if (!line.IsEmpty) + { + var caption = new FigureCaption(this) { - Span = new SourceSpan(startPosition, line.End), + Span = new SourceSpan(line.Start, line.End), Line = processor.LineIndex, - Column = column, - OpeningCharacter = openingChar, - OpeningCharacterCount = count + Column = column + line.Start - startPosition, + IsOpen = false }; + caption.AppendLine(ref line, caption.Column, processor.LineIndex, processor.CurrentLineStartPosition, processor.TrackTrivia); + figure.Add(caption); + } + processor.NewBlocks.Push(figure); + + // Discard the current line as it is already parsed + return BlockState.ContinueDiscard; + } + public override BlockState TryContinue(BlockProcessor processor, Block block) + { + var figure = (Figure)block; + int count = figure.OpeningCharacterCount; + char matchChar = figure.OpeningCharacter; + + int column = processor.Column; + // Match if we have a closing fence + var line = processor.Line; + int startPosition = line.Start; + count -= line.CountAndSkipChar(matchChar); + + // If we have a closing fence, close it and discard the current line + // The line must contain only fence opening character followed only by whitespaces. + if (count <= 0 && !processor.IsCodeIndent) + { line.TrimStart(); if (!line.IsEmpty) { @@ -64,54 +99,18 @@ public override BlockState TryOpen(BlockProcessor processor) caption.AppendLine(ref line, caption.Column, processor.LineIndex, processor.CurrentLineStartPosition, processor.TrackTrivia); figure.Add(caption); } - processor.NewBlocks.Push(figure); - // Discard the current line as it is already parsed - return BlockState.ContinueDiscard; - } + figure.UpdateSpanEnd(line.End); - public override BlockState TryContinue(BlockProcessor processor, Block block) - { - var figure = (Figure)block; - int count = figure.OpeningCharacterCount; - char matchChar = figure.OpeningCharacter; - - int column = processor.Column; - // Match if we have a closing fence - var line = processor.Line; - int startPosition = line.Start; - count -= line.CountAndSkipChar(matchChar); - - // If we have a closing fence, close it and discard the current line - // The line must contain only fence opening character followed only by whitespaces. - if (count <= 0 && !processor.IsCodeIndent) - { - line.TrimStart(); - if (!line.IsEmpty) - { - var caption = new FigureCaption(this) - { - Span = new SourceSpan(line.Start, line.End), - Line = processor.LineIndex, - Column = column + line.Start - startPosition, - IsOpen = false - }; - caption.AppendLine(ref line, caption.Column, processor.LineIndex, processor.CurrentLineStartPosition, processor.TrackTrivia); - figure.Add(caption); - } - - figure.UpdateSpanEnd(line.End); - - // Don't keep the last line - return BlockState.BreakDiscard; - } + // Don't keep the last line + return BlockState.BreakDiscard; + } - // Reset the indentation to the column before the indent - processor.GoToColumn(processor.ColumnBeforeIndent); + // Reset the indentation to the column before the indent + processor.GoToColumn(processor.ColumnBeforeIndent); - figure.UpdateSpanEnd(line.End); + figure.UpdateSpanEnd(line.End); - return BlockState.Continue; - } + return BlockState.Continue; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Figures/FigureCaption.cs b/src/Markdig/Extensions/Figures/FigureCaption.cs index ad6ce8eb0..54e47ece3 100644 --- a/src/Markdig/Extensions/Figures/FigureCaption.cs +++ b/src/Markdig/Extensions/Figures/FigureCaption.cs @@ -5,21 +5,20 @@ using Markdig.Parsers; using Markdig.Syntax; -namespace Markdig.Extensions.Figures +namespace Markdig.Extensions.Figures; + +/// +/// Defines a figure caption. +/// +/// +public class FigureCaption : LeafBlock { /// - /// Defines a figure caption. + /// Initializes a new instance of the class. /// - /// - public class FigureCaption : LeafBlock + /// The parser used to create this block. + public FigureCaption(BlockParser parser) : base(parser) { - /// - /// Initializes a new instance of the class. - /// - /// The parser used to create this block. - public FigureCaption(BlockParser parser) : base(parser) - { - ProcessInlines = true; - } + ProcessInlines = true; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Figures/FigureExtension.cs b/src/Markdig/Extensions/Figures/FigureExtension.cs index edf752609..9cb80b7a8 100644 --- a/src/Markdig/Extensions/Figures/FigureExtension.cs +++ b/src/Markdig/Extensions/Figures/FigureExtension.cs @@ -5,37 +5,36 @@ using Markdig.Extensions.Footers; using Markdig.Renderers; -namespace Markdig.Extensions.Figures +namespace Markdig.Extensions.Figures; + +/// +/// Extension to allow usage of figures and figure captions. +/// +/// +public class FigureExtension : IMarkdownExtension { - /// - /// Extension to allow usage of figures and figure captions. - /// - /// - public class FigureExtension : IMarkdownExtension + public void Setup(MarkdownPipelineBuilder pipeline) { - public void Setup(MarkdownPipelineBuilder pipeline) + if (!pipeline.BlockParsers.Contains()) { - if (!pipeline.BlockParsers.Contains()) + // The Figure extension must come before the Footer extension + if (pipeline.BlockParsers.Contains()) + { + pipeline.BlockParsers.InsertBefore(new FigureBlockParser()); + } + else { - // The Figure extension must come before the Footer extension - if (pipeline.BlockParsers.Contains()) - { - pipeline.BlockParsers.InsertBefore(new FigureBlockParser()); - } - else - { - pipeline.BlockParsers.Insert(0, new FigureBlockParser()); - } + pipeline.BlockParsers.Insert(0, new FigureBlockParser()); } } + } - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + if (renderer is HtmlRenderer htmlRenderer) { - if (renderer is HtmlRenderer htmlRenderer) - { - htmlRenderer.ObjectRenderers.AddIfNotAlready(); - htmlRenderer.ObjectRenderers.AddIfNotAlready(); - } + htmlRenderer.ObjectRenderers.AddIfNotAlready(); + htmlRenderer.ObjectRenderers.AddIfNotAlready(); } } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Figures/HtmlFigureCaptionRenderer.cs b/src/Markdig/Extensions/Figures/HtmlFigureCaptionRenderer.cs index cb4713f1c..c4c18be3a 100644 --- a/src/Markdig/Extensions/Figures/HtmlFigureCaptionRenderer.cs +++ b/src/Markdig/Extensions/Figures/HtmlFigureCaptionRenderer.cs @@ -5,20 +5,19 @@ using Markdig.Renderers; using Markdig.Renderers.Html; -namespace Markdig.Extensions.Figures +namespace Markdig.Extensions.Figures; + +/// +/// A HTML renderer for a . +/// +/// +public class HtmlFigureCaptionRenderer : HtmlObjectRenderer { - /// - /// A HTML renderer for a . - /// - /// - public class HtmlFigureCaptionRenderer : HtmlObjectRenderer + protected override void Write(HtmlRenderer renderer, FigureCaption obj) { - protected override void Write(HtmlRenderer renderer, FigureCaption obj) - { - renderer.EnsureLine(); - renderer.Write("'); - renderer.WriteLeafInline(obj); - renderer.WriteLine(""); - } + renderer.EnsureLine(); + renderer.Write("'); + renderer.WriteLeafInline(obj); + renderer.WriteLine(""); } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Figures/HtmlFigureRenderer.cs b/src/Markdig/Extensions/Figures/HtmlFigureRenderer.cs index 02fd727df..b7de79967 100644 --- a/src/Markdig/Extensions/Figures/HtmlFigureRenderer.cs +++ b/src/Markdig/Extensions/Figures/HtmlFigureRenderer.cs @@ -5,20 +5,19 @@ using Markdig.Renderers; using Markdig.Renderers.Html; -namespace Markdig.Extensions.Figures +namespace Markdig.Extensions.Figures; + +/// +/// A HTML renderer for a . +/// +/// +public class HtmlFigureRenderer : HtmlObjectRenderer
    { - /// - /// A HTML renderer for a . - /// - /// - public class HtmlFigureRenderer : HtmlObjectRenderer
    + protected override void Write(HtmlRenderer renderer, Figure obj) { - protected override void Write(HtmlRenderer renderer, Figure obj) - { - renderer.EnsureLine(); - renderer.Write(""); - renderer.WriteChildren(obj); - renderer.WriteLine("
    "); - } + renderer.EnsureLine(); + renderer.Write(""); + renderer.WriteChildren(obj); + renderer.WriteLine("
    "); } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Footers/FooterBlock.cs b/src/Markdig/Extensions/Footers/FooterBlock.cs index cb53d57ac..287050aff 100644 --- a/src/Markdig/Extensions/Footers/FooterBlock.cs +++ b/src/Markdig/Extensions/Footers/FooterBlock.cs @@ -1,29 +1,28 @@ -// Copyright (c) Alexandre Mutel. All rights reserved. +// 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.Parsers; using Markdig.Syntax; -namespace Markdig.Extensions.Footers +namespace Markdig.Extensions.Footers; + +/// +/// A block element for a footer. +/// +/// +public class FooterBlock : ContainerBlock { /// - /// A block element for a footer. + /// Initializes a new instance of the class. /// - /// - public class FooterBlock : ContainerBlock + /// The parser used to create this block. + public FooterBlock(BlockParser parser) : base(parser) { - /// - /// Initializes a new instance of the class. - /// - /// The parser used to create this block. - public FooterBlock(BlockParser parser) : base(parser) - { - } - - /// - /// Gets or sets the opening character used to match this footer (by default it is ^) - /// - public char OpeningCharacter { get; set; } } + + /// + /// Gets or sets the opening character used to match this footer (by default it is ^) + /// + public char OpeningCharacter { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Footers/FooterBlockParser.cs b/src/Markdig/Extensions/Footers/FooterBlockParser.cs index ffcdf5164..8ab204445 100644 --- a/src/Markdig/Extensions/Footers/FooterBlockParser.cs +++ b/src/Markdig/Extensions/Footers/FooterBlockParser.cs @@ -1,4 +1,4 @@ -// Copyright (c) Alexandre Mutel. All rights reserved. +// 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. @@ -6,83 +6,82 @@ using Markdig.Parsers; using Markdig.Syntax; -namespace Markdig.Extensions.Footers +namespace Markdig.Extensions.Footers; + +/// +/// A block parser for a . +/// +/// +public class FooterBlockParser : BlockParser { /// - /// A block parser for a . + /// Initializes a new instance of the class. /// - /// - public class FooterBlockParser : BlockParser + public FooterBlockParser() { - /// - /// Initializes a new instance of the class. - /// - public FooterBlockParser() - { - OpeningCharacters = new[] {'^'}; - } + OpeningCharacters = new[] {'^'}; + } - public override BlockState TryOpen(BlockProcessor processor) + public override BlockState TryOpen(BlockProcessor processor) + { + if (processor.IsCodeIndent) { - if (processor.IsCodeIndent) - { - return BlockState.None; - } + return BlockState.None; + } - var column = processor.Column; - var startPosition = processor.Start; + var column = processor.Column; + var startPosition = processor.Start; - // A footer - // A Footer marker consists of 0-3 spaces of initial indent, plus (a) the characters ^^ together with a following space, or (b) a double character ^^ not followed by a space. - var openingChar = processor.CurrentChar; - if (processor.PeekChar(1) != openingChar) - { - return BlockState.None; - } - processor.NextChar(); // Grab 2nd^ - var c = processor.NextChar(); // grab space - if (c.IsSpaceOrTab()) - { - processor.NextColumn(); - } - processor.NewBlocks.Push(new FooterBlock(this) - { - Span = new SourceSpan(startPosition, processor.Line.End), - OpeningCharacter = openingChar, - Column = column, - Line = processor.LineIndex, - }); - return BlockState.Continue; + // A footer + // A Footer marker consists of 0-3 spaces of initial indent, plus (a) the characters ^^ together with a following space, or (b) a double character ^^ not followed by a space. + var openingChar = processor.CurrentChar; + if (processor.PeekChar(1) != openingChar) + { + return BlockState.None; } + processor.NextChar(); // Grab 2nd^ + var c = processor.NextChar(); // grab space + if (c.IsSpaceOrTab()) + { + processor.NextColumn(); + } + processor.NewBlocks.Push(new FooterBlock(this) + { + Span = new SourceSpan(startPosition, processor.Line.End), + OpeningCharacter = openingChar, + Column = column, + Line = processor.LineIndex, + }); + return BlockState.Continue; + } - public override BlockState TryContinue(BlockProcessor processor, Block block) + public override BlockState TryContinue(BlockProcessor processor, Block block) + { + if (processor.IsCodeIndent) { - if (processor.IsCodeIndent) - { - return BlockState.None; - } + return BlockState.None; + } - var quote = (FooterBlock) block; + var quote = (FooterBlock) block; - // A footer - // A Footer marker consists of 0-3 spaces of initial indent, plus (a) the characters ^^ together with a following space, or (b) a double character ^^ not followed by a space. - var c = processor.CurrentChar; - var result = BlockState.Continue; - if (c != quote.OpeningCharacter || processor.PeekChar(1) != c) - { - result = processor.IsBlankLine ? BlockState.BreakDiscard : BlockState.None; - } - else + // A footer + // A Footer marker consists of 0-3 spaces of initial indent, plus (a) the characters ^^ together with a following space, or (b) a double character ^^ not followed by a space. + var c = processor.CurrentChar; + var result = BlockState.Continue; + if (c != quote.OpeningCharacter || processor.PeekChar(1) != c) + { + result = processor.IsBlankLine ? BlockState.BreakDiscard : BlockState.None; + } + else + { + processor.NextChar(); // Skip ^^ char (1st) + c = processor.NextChar(); // Skip ^^ char (2nd) + if (c.IsSpace()) { - processor.NextChar(); // Skip ^^ char (1st) - c = processor.NextChar(); // Skip ^^ char (2nd) - if (c.IsSpace()) - { - processor.NextChar(); // Skip following space - } - block.UpdateSpanEnd(processor.Line.End); + processor.NextChar(); // Skip following space } - return result; + block.UpdateSpanEnd(processor.Line.End); } + return result; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Footers/FooterExtension.cs b/src/Markdig/Extensions/Footers/FooterExtension.cs index 71dbbbb3d..2b07de546 100644 --- a/src/Markdig/Extensions/Footers/FooterExtension.cs +++ b/src/Markdig/Extensions/Footers/FooterExtension.cs @@ -5,36 +5,35 @@ using Markdig.Extensions.Figures; using Markdig.Renderers; -namespace Markdig.Extensions.Footers +namespace Markdig.Extensions.Footers; + +/// +/// Extension that provides footer. +/// +/// +public class FooterExtension : IMarkdownExtension { - /// - /// Extension that provides footer. - /// - /// - public class FooterExtension : IMarkdownExtension + public void Setup(MarkdownPipelineBuilder pipeline) { - public void Setup(MarkdownPipelineBuilder pipeline) + if (!pipeline.BlockParsers.Contains()) { - if (!pipeline.BlockParsers.Contains()) + // The Figure extension must come before the Footer extension + if (pipeline.BlockParsers.Contains()) + { + pipeline.BlockParsers.InsertAfter(new FooterBlockParser()); + } + else { - // The Figure extension must come before the Footer extension - if (pipeline.BlockParsers.Contains()) - { - pipeline.BlockParsers.InsertAfter(new FooterBlockParser()); - } - else - { - pipeline.BlockParsers.Insert(0, new FooterBlockParser()); - } + pipeline.BlockParsers.Insert(0, new FooterBlockParser()); } } + } - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + if (renderer is HtmlRenderer htmlRenderer) { - if (renderer is HtmlRenderer htmlRenderer) - { - htmlRenderer.ObjectRenderers.AddIfNotAlready(new HtmlFooterBlockRenderer()); - } + htmlRenderer.ObjectRenderers.AddIfNotAlready(new HtmlFooterBlockRenderer()); } } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Footers/HtmlFooterRenderer.cs b/src/Markdig/Extensions/Footers/HtmlFooterRenderer.cs index b690c460e..6f5e8c2e1 100644 --- a/src/Markdig/Extensions/Footers/HtmlFooterRenderer.cs +++ b/src/Markdig/Extensions/Footers/HtmlFooterRenderer.cs @@ -5,23 +5,22 @@ using Markdig.Renderers; using Markdig.Renderers.Html; -namespace Markdig.Extensions.Footers +namespace Markdig.Extensions.Footers; + +/// +/// A HTML renderer for a . +/// +/// +public class HtmlFooterBlockRenderer : HtmlObjectRenderer { - /// - /// A HTML renderer for a . - /// - /// - public class HtmlFooterBlockRenderer : HtmlObjectRenderer + protected override void Write(HtmlRenderer renderer, FooterBlock footer) { - protected override void Write(HtmlRenderer renderer, FooterBlock footer) - { - renderer.EnsureLine(); - renderer.Write(""); - var implicitParagraph = renderer.ImplicitParagraph; - renderer.ImplicitParagraph = true; - renderer.WriteChildren(footer); - renderer.ImplicitParagraph = implicitParagraph; - renderer.WriteLine(""); - } + renderer.EnsureLine(); + renderer.Write(""); + var implicitParagraph = renderer.ImplicitParagraph; + renderer.ImplicitParagraph = true; + renderer.WriteChildren(footer); + renderer.ImplicitParagraph = implicitParagraph; + renderer.WriteLine(""); } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Footnotes/Footnote.cs b/src/Markdig/Extensions/Footnotes/Footnote.cs index 8dad14ad6..37c416bb3 100644 --- a/src/Markdig/Extensions/Footnotes/Footnote.cs +++ b/src/Markdig/Extensions/Footnotes/Footnote.cs @@ -2,43 +2,41 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using System.Collections.Generic; using Markdig.Parsers; using Markdig.Syntax; -namespace Markdig.Extensions.Footnotes +namespace Markdig.Extensions.Footnotes; + +/// +/// A block for a footnote. +/// +/// +public class Footnote : ContainerBlock { - /// - /// A block for a footnote. - /// - /// - public class Footnote : ContainerBlock + public Footnote(BlockParser parser) : base(parser) { - public Footnote(BlockParser parser) : base(parser) - { - Order = -1; - } - - /// - /// Gets or sets the label used by this footnote. - /// - public string? Label { get; set; } - - /// - /// Gets or sets the order of this footnote (determined by the order of the in the document) - /// - public int Order { get; set; } - - /// - /// Gets the links referencing this footnote. - /// - public List Links { get; } = new (); - - /// - /// The label span - /// - public SourceSpan LabelSpan; - - internal bool IsLastLineEmpty { get; set; } + Order = -1; } + + /// + /// Gets or sets the label used by this footnote. + /// + public string? Label { get; set; } + + /// + /// Gets or sets the order of this footnote (determined by the order of the in the document) + /// + public int Order { get; set; } + + /// + /// Gets the links referencing this footnote. + /// + public List Links { get; } = new (); + + /// + /// The label span + /// + public SourceSpan LabelSpan; + + internal bool IsLastLineEmpty { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Footnotes/FootnoteExtension.cs b/src/Markdig/Extensions/Footnotes/FootnoteExtension.cs index c81385c61..897e4aa98 100644 --- a/src/Markdig/Extensions/Footnotes/FootnoteExtension.cs +++ b/src/Markdig/Extensions/Footnotes/FootnoteExtension.cs @@ -4,30 +4,29 @@ using Markdig.Renderers; -namespace Markdig.Extensions.Footnotes +namespace Markdig.Extensions.Footnotes; + +/// +/// Extension to allow footnotes. +/// +/// +public class FootnoteExtension : IMarkdownExtension { - /// - /// Extension to allow footnotes. - /// - /// - public class FootnoteExtension : IMarkdownExtension + public void Setup(MarkdownPipelineBuilder pipeline) { - public void Setup(MarkdownPipelineBuilder pipeline) + if (!pipeline.BlockParsers.Contains()) { - if (!pipeline.BlockParsers.Contains()) - { - // Insert the parser before any other parsers - pipeline.BlockParsers.Insert(0, new FootnoteParser()); - } + // Insert the parser before any other parsers + pipeline.BlockParsers.Insert(0, new FootnoteParser()); } + } - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + if (renderer is HtmlRenderer htmlRenderer) { - if (renderer is HtmlRenderer htmlRenderer) - { - htmlRenderer.ObjectRenderers.AddIfNotAlready(new HtmlFootnoteGroupRenderer()); - htmlRenderer.ObjectRenderers.AddIfNotAlready(new HtmlFootnoteLinkRenderer()); - } + htmlRenderer.ObjectRenderers.AddIfNotAlready(new HtmlFootnoteGroupRenderer()); + htmlRenderer.ObjectRenderers.AddIfNotAlready(new HtmlFootnoteLinkRenderer()); } } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Footnotes/FootnoteGroup.cs b/src/Markdig/Extensions/Footnotes/FootnoteGroup.cs index c45fec716..df5e6d9db 100644 --- a/src/Markdig/Extensions/Footnotes/FootnoteGroup.cs +++ b/src/Markdig/Extensions/Footnotes/FootnoteGroup.cs @@ -5,22 +5,21 @@ using Markdig.Parsers; using Markdig.Syntax; -namespace Markdig.Extensions.Footnotes +namespace Markdig.Extensions.Footnotes; + +/// +/// A block that contains all the footnotes at the end of a . +/// +/// +public class FootnoteGroup : ContainerBlock { /// - /// A block that contains all the footnotes at the end of a . + /// Initializes a new instance of the class. /// - /// - public class FootnoteGroup : ContainerBlock + /// The parser used to create this block. + public FootnoteGroup(BlockParser parser) : base(parser) { - /// - /// Initializes a new instance of the class. - /// - /// The parser used to create this block. - public FootnoteGroup(BlockParser parser) : base(parser) - { - } - - internal int CurrentOrder { get; set; } } + + internal int CurrentOrder { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Footnotes/FootnoteLink.cs b/src/Markdig/Extensions/Footnotes/FootnoteLink.cs index a65c99eb3..9e270e21e 100644 --- a/src/Markdig/Extensions/Footnotes/FootnoteLink.cs +++ b/src/Markdig/Extensions/Footnotes/FootnoteLink.cs @@ -4,32 +4,31 @@ using Markdig.Syntax.Inlines; -namespace Markdig.Extensions.Footnotes +namespace Markdig.Extensions.Footnotes; + +/// +/// A inline link to a . +/// +/// +public class FootnoteLink : Inline { - /// - /// A inline link to a . - /// - /// - public class FootnoteLink : Inline + public FootnoteLink(Footnote footnote) { - public FootnoteLink(Footnote footnote) - { - Footnote = footnote; - } + Footnote = footnote; + } - /// - /// Gets or sets a value indicating whether this instance is back link (from a footnote to the link) - /// - public bool IsBackLink { get; set; } + /// + /// Gets or sets a value indicating whether this instance is back link (from a footnote to the link) + /// + public bool IsBackLink { get; set; } - /// - /// Gets or sets the global index number of this link. - /// - public int Index { get; set; } + /// + /// Gets or sets the global index number of this link. + /// + public int Index { get; set; } - /// - /// Gets or sets the footnote this link refers to. - /// - public Footnote Footnote { get; set; } - } + /// + /// Gets or sets the footnote this link refers to. + /// + public Footnote Footnote { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Footnotes/FootnoteLinkReferenceDefinition.cs b/src/Markdig/Extensions/Footnotes/FootnoteLinkReferenceDefinition.cs index f025ec001..700bc8e7d 100644 --- a/src/Markdig/Extensions/Footnotes/FootnoteLinkReferenceDefinition.cs +++ b/src/Markdig/Extensions/Footnotes/FootnoteLinkReferenceDefinition.cs @@ -4,22 +4,21 @@ using Markdig.Syntax; -namespace Markdig.Extensions.Footnotes +namespace Markdig.Extensions.Footnotes; + +/// +/// A link reference definition stored at the level. +/// +/// +public class FootnoteLinkReferenceDefinition : LinkReferenceDefinition { - /// - /// A link reference definition stored at the level. - /// - /// - public class FootnoteLinkReferenceDefinition : LinkReferenceDefinition + public FootnoteLinkReferenceDefinition(Footnote footnote) { - public FootnoteLinkReferenceDefinition(Footnote footnote) - { - Footnote = footnote; - } - - /// - /// Gets or sets the footnote related to this link reference definition. - /// - public Footnote Footnote { get; set; } + Footnote = footnote; } + + /// + /// Gets or sets the footnote related to this link reference definition. + /// + public Footnote Footnote { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Footnotes/FootnoteParser.cs b/src/Markdig/Extensions/Footnotes/FootnoteParser.cs index f66fcf397..c393cb42b 100644 --- a/src/Markdig/Extensions/Footnotes/FootnoteParser.cs +++ b/src/Markdig/Extensions/Footnotes/FootnoteParser.cs @@ -7,202 +7,201 @@ using Markdig.Syntax; using Markdig.Syntax.Inlines; -namespace Markdig.Extensions.Footnotes +namespace Markdig.Extensions.Footnotes; + +/// +/// The block parser for a . +/// +/// +public class FootnoteParser : BlockParser { /// - /// The block parser for a . + /// The key used to store at the document level the pending /// - /// - public class FootnoteParser : BlockParser + private static readonly object DocumentKey = typeof(Footnote); + + public FootnoteParser() { - /// - /// The key used to store at the document level the pending - /// - private static readonly object DocumentKey = typeof(Footnote); + OpeningCharacters = new [] {'['}; + } - public FootnoteParser() - { - OpeningCharacters = new [] {'['}; - } + public override BlockState TryOpen(BlockProcessor processor) + { + return TryOpen(processor, false); + } - public override BlockState TryOpen(BlockProcessor processor) + private BlockState TryOpen(BlockProcessor processor, bool isContinue) + { + // We expect footnote to appear only at document level and not indented more than a code indent block + var currentContainer = processor.GetCurrentContainerOpened(); + if (processor.IsCodeIndent || (!isContinue && currentContainer.GetType() != typeof(MarkdownDocument)) || (isContinue && !(currentContainer is FootnoteGroup))) { - return TryOpen(processor, false); + return BlockState.None; } - private BlockState TryOpen(BlockProcessor processor, bool isContinue) + var saved = processor.Column; + int start = processor.Start; + if (!LinkHelper.TryParseLabel(ref processor.Line, false, out string? label, out SourceSpan labelSpan) || !label.StartsWith("^") || processor.CurrentChar != ':') { - // We expect footnote to appear only at document level and not indented more than a code indent block - var currentContainer = processor.GetCurrentContainerOpened(); - if (processor.IsCodeIndent || (!isContinue && currentContainer.GetType() != typeof(MarkdownDocument)) || (isContinue && !(currentContainer is FootnoteGroup))) - { - return BlockState.None; - } + processor.GoToColumn(saved); + return BlockState.None; + } - var saved = processor.Column; - int start = processor.Start; - if (!LinkHelper.TryParseLabel(ref processor.Line, false, out string? label, out SourceSpan labelSpan) || !label.StartsWith("^") || processor.CurrentChar != ':') - { - processor.GoToColumn(saved); - return BlockState.None; - } + // Advance the column + int deltaColumn = processor.Start - start; + processor.Column = processor.Column + deltaColumn; - // Advance the column - int deltaColumn = processor.Start - start; - processor.Column = processor.Column + deltaColumn; + processor.NextChar(); // Skip ':' - processor.NextChar(); // Skip ':' + var footnote = new Footnote(this) + { + Label = label, + LabelSpan = labelSpan, + }; - var footnote = new Footnote(this) - { - Label = label, - LabelSpan = labelSpan, - }; + // Maintain a list of all footnotes at document level + var footnotes = processor.Document.GetData(DocumentKey) as FootnoteGroup; + if (footnotes == null) + { + footnotes = new FootnoteGroup(this); + processor.Document.Add(footnotes); + processor.Document.SetData(DocumentKey, footnotes); + processor.Document.ProcessInlinesEnd += Document_ProcessInlinesEnd; + } + footnotes.Add(footnote); - // Maintain a list of all footnotes at document level - var footnotes = processor.Document.GetData(DocumentKey) as FootnoteGroup; - if (footnotes == null) - { - footnotes = new FootnoteGroup(this); - processor.Document.Add(footnotes); - processor.Document.SetData(DocumentKey, footnotes); - processor.Document.ProcessInlinesEnd += Document_ProcessInlinesEnd; - } - footnotes.Add(footnote); + var linkRef = new FootnoteLinkReferenceDefinition(footnote) + { + CreateLinkInline = CreateLinkToFootnote, + Line = processor.LineIndex, + Span = new SourceSpan(start, processor.Start - 2), // account for ]: + LabelSpan = labelSpan, + Label = label + }; + processor.Document.SetLinkReferenceDefinition(footnote.Label, linkRef, true); + processor.NewBlocks.Push(footnote); + return BlockState.Continue; + } - var linkRef = new FootnoteLinkReferenceDefinition(footnote) - { - CreateLinkInline = CreateLinkToFootnote, - Line = processor.LineIndex, - Span = new SourceSpan(start, processor.Start - 2), // account for ]: - LabelSpan = labelSpan, - Label = label - }; - processor.Document.SetLinkReferenceDefinition(footnote.Label, linkRef, true); - processor.NewBlocks.Push(footnote); - return BlockState.Continue; - } + public override BlockState TryContinue(BlockProcessor processor, Block block) + { + var footnote = (Footnote) block; - public override BlockState TryContinue(BlockProcessor processor, Block block) + if (processor.CurrentBlock == null || processor.CurrentBlock.IsBreakable) { - var footnote = (Footnote) block; + if (processor.IsBlankLine) + { + footnote.IsLastLineEmpty = true; + return BlockState.ContinueDiscard; + } - if (processor.CurrentBlock == null || processor.CurrentBlock.IsBreakable) + if (processor.Column == 0) { - if (processor.IsBlankLine) + if (footnote.IsLastLineEmpty) { - footnote.IsLastLineEmpty = true; - return BlockState.ContinueDiscard; + // Close the current footnote + processor.Close(footnote); + + // Parse any opening footnote + return TryOpen(processor); } - if (processor.Column == 0) + // Make sure that consecutive footnotes without a blanklines are parsed correctly + if (TryOpen(processor, true) == BlockState.Continue) { - if (footnote.IsLastLineEmpty) - { - // Close the current footnote - processor.Close(footnote); - - // Parse any opening footnote - return TryOpen(processor); - } - - // Make sure that consecutive footnotes without a blanklines are parsed correctly - if (TryOpen(processor, true) == BlockState.Continue) - { - processor.Close(footnote); - return BlockState.Continue; - } + processor.Close(footnote); + return BlockState.Continue; } } - footnote.IsLastLineEmpty = false; - - if (processor.IsCodeIndent) - { - processor.GoToCodeIndent(); - } - - return BlockState.Continue; } + footnote.IsLastLineEmpty = false; - /// - /// Add footnotes to the end of the document - /// - /// The processor. - /// The inline. - private void Document_ProcessInlinesEnd(InlineProcessor state, Inline? inline) + if (processor.IsCodeIndent) { - // Unregister - state.Document.ProcessInlinesEnd -= Document_ProcessInlinesEnd; + processor.GoToCodeIndent(); + } - var footnotes = (FootnoteGroup)state.Document.GetData(DocumentKey)!; - // Remove the footnotes from the document and readd them at the end - state.Document.Remove(footnotes); - state.Document.Add(footnotes); - state.Document.RemoveData(DocumentKey); + return BlockState.Continue; + } - footnotes.Sort( - (leftObj, rightObj) => - { - var left = (Footnote)leftObj; - var right = (Footnote)rightObj; + /// + /// Add footnotes to the end of the document + /// + /// The processor. + /// The inline. + private void Document_ProcessInlinesEnd(InlineProcessor state, Inline? inline) + { + // Unregister + state.Document.ProcessInlinesEnd -= Document_ProcessInlinesEnd; - return left.Order >= 0 && right.Order >= 0 - ? left.Order.CompareTo(right.Order) - : 0; - }); + var footnotes = (FootnoteGroup)state.Document.GetData(DocumentKey)!; + // Remove the footnotes from the document and readd them at the end + state.Document.Remove(footnotes); + state.Document.Add(footnotes); + state.Document.RemoveData(DocumentKey); - int linkIndex = 0; - for (int i = 0; i < footnotes.Count; i++) + footnotes.Sort( + (leftObj, rightObj) => { - var footnote = (Footnote)footnotes[i]; - if (footnote.Order < 0) - { - // Remove this footnote if it doesn't have any links - footnotes.RemoveAt(i); - i--; - continue; - } + var left = (Footnote)leftObj; + var right = (Footnote)rightObj; - // Insert all footnote backlinks - var paragraphBlock = footnote.LastChild as ParagraphBlock; - if (paragraphBlock is null) - { - paragraphBlock = new ParagraphBlock(); - footnote.Add(paragraphBlock); - } - if (paragraphBlock.Inline == null) - { - paragraphBlock.Inline = new ContainerInline(); - } - - foreach (var link in footnote.Links) - { - linkIndex++; - link.Index = linkIndex; - var backLink = new FootnoteLink(footnote) - { - Index = linkIndex, - IsBackLink = true - }; - paragraphBlock.Inline.AppendChild(backLink); - } - } - } + return left.Order >= 0 && right.Order >= 0 + ? left.Order.CompareTo(right.Order) + : 0; + }); - private static Inline CreateLinkToFootnote(InlineProcessor state, LinkReferenceDefinition linkRef, Inline? child) + int linkIndex = 0; + for (int i = 0; i < footnotes.Count; i++) { - var footnote = ((FootnoteLinkReferenceDefinition)linkRef).Footnote; + var footnote = (Footnote)footnotes[i]; if (footnote.Order < 0) { - var footnotes = (FootnoteGroup)state.Document.GetData(DocumentKey)!; - footnotes.CurrentOrder++; - footnote.Order = footnotes.CurrentOrder; + // Remove this footnote if it doesn't have any links + footnotes.RemoveAt(i); + i--; + continue; + } + + // Insert all footnote backlinks + var paragraphBlock = footnote.LastChild as ParagraphBlock; + if (paragraphBlock is null) + { + paragraphBlock = new ParagraphBlock(); + footnote.Add(paragraphBlock); + } + if (paragraphBlock.Inline == null) + { + paragraphBlock.Inline = new ContainerInline(); } - var link = new FootnoteLink(footnote); - footnote.Links.Add(link); + foreach (var link in footnote.Links) + { + linkIndex++; + link.Index = linkIndex; + var backLink = new FootnoteLink(footnote) + { + Index = linkIndex, + IsBackLink = true + }; + paragraphBlock.Inline.AppendChild(backLink); + } + } + } - return link; + private static Inline CreateLinkToFootnote(InlineProcessor state, LinkReferenceDefinition linkRef, Inline? child) + { + var footnote = ((FootnoteLinkReferenceDefinition)linkRef).Footnote; + if (footnote.Order < 0) + { + var footnotes = (FootnoteGroup)state.Document.GetData(DocumentKey)!; + footnotes.CurrentOrder++; + footnote.Order = footnotes.CurrentOrder; } + + var link = new FootnoteLink(footnote); + footnote.Links.Add(link); + + return link; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Footnotes/HtmlFootnoteGroupRenderer.cs b/src/Markdig/Extensions/Footnotes/HtmlFootnoteGroupRenderer.cs index d05c9327f..58a764d16 100644 --- a/src/Markdig/Extensions/Footnotes/HtmlFootnoteGroupRenderer.cs +++ b/src/Markdig/Extensions/Footnotes/HtmlFootnoteGroupRenderer.cs @@ -5,43 +5,42 @@ using Markdig.Renderers; using Markdig.Renderers.Html; -namespace Markdig.Extensions.Footnotes +namespace Markdig.Extensions.Footnotes; + +/// +/// A HTML renderer for a . +/// +/// +public class HtmlFootnoteGroupRenderer : HtmlObjectRenderer { /// - /// A HTML renderer for a . + /// Initializes a new instance of the class. /// - /// - public class HtmlFootnoteGroupRenderer : HtmlObjectRenderer + public HtmlFootnoteGroupRenderer() { - /// - /// Initializes a new instance of the class. - /// - public HtmlFootnoteGroupRenderer() - { - GroupClass = "footnotes"; - } + GroupClass = "footnotes"; + } - /// - /// Gets or sets the CSS group class used when rendering the <div> of this instance. - /// - public string GroupClass { get; set; } + /// + /// Gets or sets the CSS group class used when rendering the <div> of this instance. + /// + public string GroupClass { get; set; } - protected override void Write(HtmlRenderer renderer, FootnoteGroup footnotes) - { - renderer.EnsureLine(); - renderer.WriteLine($"
    "); - renderer.WriteLine("
    "); - renderer.WriteLine("
      "); + protected override void Write(HtmlRenderer renderer, FootnoteGroup footnotes) + { + renderer.EnsureLine(); + renderer.WriteLine($"
      "); + renderer.WriteLine("
      "); + renderer.WriteLine("
        "); - for (int i = 0; i < footnotes.Count; i++) - { - var footnote = (Footnote)footnotes[i]; - renderer.WriteLine($"
      1. "); - renderer.WriteChildren(footnote); - renderer.WriteLine("
      2. "); - } - renderer.WriteLine("
      "); - renderer.WriteLine("
      "); + for (int i = 0; i < footnotes.Count; i++) + { + var footnote = (Footnote)footnotes[i]; + renderer.WriteLine($"
    1. "); + renderer.WriteChildren(footnote); + renderer.WriteLine("
    2. "); } + renderer.WriteLine("
    "); + renderer.WriteLine("
    "); } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Footnotes/HtmlFootnoteLinkRenderer.cs b/src/Markdig/Extensions/Footnotes/HtmlFootnoteLinkRenderer.cs index 98328200e..8a3b69378 100644 --- a/src/Markdig/Extensions/Footnotes/HtmlFootnoteLinkRenderer.cs +++ b/src/Markdig/Extensions/Footnotes/HtmlFootnoteLinkRenderer.cs @@ -5,32 +5,31 @@ using Markdig.Renderers; using Markdig.Renderers.Html; -namespace Markdig.Extensions.Footnotes +namespace Markdig.Extensions.Footnotes; + +/// +/// A HTML renderer for a . +/// +/// +public class HtmlFootnoteLinkRenderer : HtmlObjectRenderer { - /// - /// A HTML renderer for a . - /// - /// - public class HtmlFootnoteLinkRenderer : HtmlObjectRenderer + public HtmlFootnoteLinkRenderer() { - public HtmlFootnoteLinkRenderer() - { - BackLinkString = "↩"; - FootnoteLinkClass = "footnote-ref"; - FootnoteBackLinkClass = "footnote-back-ref"; - } - public string BackLinkString { get; set; } + BackLinkString = "↩"; + FootnoteLinkClass = "footnote-ref"; + FootnoteBackLinkClass = "footnote-back-ref"; + } + public string BackLinkString { get; set; } - public string FootnoteLinkClass { get; set; } + public string FootnoteLinkClass { get; set; } - public string FootnoteBackLinkClass { get; set; } + public string FootnoteBackLinkClass { get; set; } - protected override void Write(HtmlRenderer renderer, FootnoteLink link) - { - var order = link.Footnote.Order; - renderer.Write(link.IsBackLink - ? $"
    {BackLinkString}" - : $"{order}"); - } + protected override void Write(HtmlRenderer renderer, FootnoteLink link) + { + var order = link.Footnote.Order; + renderer.Write(link.IsBackLink + ? $"{BackLinkString}" + : $"{order}"); } } \ No newline at end of file diff --git a/src/Markdig/Extensions/GenericAttributes/GenericAttributesExtension.cs b/src/Markdig/Extensions/GenericAttributes/GenericAttributesExtension.cs index d09899ac1..85310812e 100644 --- a/src/Markdig/Extensions/GenericAttributes/GenericAttributesExtension.cs +++ b/src/Markdig/Extensions/GenericAttributes/GenericAttributesExtension.cs @@ -9,65 +9,64 @@ using Markdig.Syntax; using Markdig.Syntax.Inlines; -namespace Markdig.Extensions.GenericAttributes +namespace Markdig.Extensions.GenericAttributes; + +/// +/// Extension that allows to attach HTML attributes to the previous or current . +/// This extension should be enabled last after enabling other extensions. +/// +/// +public class GenericAttributesExtension : IMarkdownExtension { - /// - /// Extension that allows to attach HTML attributes to the previous or current . - /// This extension should be enabled last after enabling other extensions. - /// - /// - public class GenericAttributesExtension : IMarkdownExtension + public void Setup(MarkdownPipelineBuilder pipeline) { - public void Setup(MarkdownPipelineBuilder pipeline) + if (!pipeline.InlineParsers.Contains()) { - if (!pipeline.InlineParsers.Contains()) - { - pipeline.InlineParsers.Insert(0, new GenericAttributesParser()); - } + pipeline.InlineParsers.Insert(0, new GenericAttributesParser()); + } - // Plug into all IAttributesParseable - foreach (var parser in pipeline.BlockParsers) + // Plug into all IAttributesParseable + foreach (var parser in pipeline.BlockParsers) + { + if (parser is IAttributesParseable attributesParseable) { - if (parser is IAttributesParseable attributesParseable) - { - attributesParseable.TryParseAttributes = TryProcessAttributesForHeading; - } + attributesParseable.TryParseAttributes = TryProcessAttributesForHeading; } } + } - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) - { - } + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + } - private bool TryProcessAttributesForHeading(BlockProcessor processor, ref StringSlice line, IBlock block) + private bool TryProcessAttributesForHeading(BlockProcessor processor, ref StringSlice line, IBlock block) + { + // Try to find if there is any attributes { in the info string on the first line of a FencedCodeBlock + if (line.Start < line.End) { - // Try to find if there is any attributes { in the info string on the first line of a FencedCodeBlock - if (line.Start < line.End) + int indexOfAttributes = line.IndexOf('{'); + if (indexOfAttributes >= 0) { - int indexOfAttributes = line.IndexOf('{'); - if (indexOfAttributes >= 0) + // Work on a copy + var copy = line; + copy.Start = indexOfAttributes; + var startOfAttributes = copy.Start; + if (GenericAttributesParser.TryParse(ref copy, out HtmlAttributes? attributes)) { - // Work on a copy - var copy = line; - copy.Start = indexOfAttributes; - var startOfAttributes = copy.Start; - if (GenericAttributesParser.TryParse(ref copy, out HtmlAttributes? attributes)) - { - var htmlAttributes = block.GetAttributes(); - attributes.CopyTo(htmlAttributes); + var htmlAttributes = block.GetAttributes(); + attributes.CopyTo(htmlAttributes); - // Update position for HtmlAttributes - htmlAttributes.Line = processor.LineIndex; - htmlAttributes.Column = startOfAttributes - processor.CurrentLineStartPosition; // This is not accurate with tabs! - htmlAttributes.Span.Start = startOfAttributes; - htmlAttributes.Span.End = copy.Start - 1; + // Update position for HtmlAttributes + htmlAttributes.Line = processor.LineIndex; + htmlAttributes.Column = startOfAttributes - processor.CurrentLineStartPosition; // This is not accurate with tabs! + htmlAttributes.Span.Start = startOfAttributes; + htmlAttributes.Span.End = copy.Start - 1; - line.End = indexOfAttributes - 1; - return true; - } + line.End = indexOfAttributes - 1; + return true; } } - return false; } + return false; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/GenericAttributes/GenericAttributesParser.cs b/src/Markdig/Extensions/GenericAttributes/GenericAttributesParser.cs index f5c2cad51..e719335f8 100644 --- a/src/Markdig/Extensions/GenericAttributes/GenericAttributesParser.cs +++ b/src/Markdig/Extensions/GenericAttributes/GenericAttributesParser.cs @@ -2,7 +2,6 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Markdig.Helpers; @@ -11,266 +10,265 @@ using Markdig.Syntax; using Markdig.Syntax.Inlines; -namespace Markdig.Extensions.GenericAttributes +namespace Markdig.Extensions.GenericAttributes; + +/// +/// An inline parser used to parse a HTML attributes that can be attached to the previous or current . +/// +/// +public class GenericAttributesParser : InlineParser { /// - /// An inline parser used to parse a HTML attributes that can be attached to the previous or current . + /// Initializes a new instance of the class. /// - /// - public class GenericAttributesParser : InlineParser + public GenericAttributesParser() { - /// - /// Initializes a new instance of the class. - /// - public GenericAttributesParser() - { - OpeningCharacters = new[] { '{' }; - } + OpeningCharacters = new[] { '{' }; + } - public override bool Match(InlineProcessor processor, ref StringSlice slice) + public override bool Match(InlineProcessor processor, ref StringSlice slice) + { + var startPosition = slice.Start; + if (TryParse(ref slice, out HtmlAttributes? attributes)) { - var startPosition = slice.Start; - if (TryParse(ref slice, out HtmlAttributes? attributes)) - { - var inline = processor.Inline; + var inline = processor.Inline; - // If the current object to attach is either a literal or delimiter - // try to find a suitable parent, otherwise attach the html attributes to the block - if (inline is LiteralInline) + // If the current object to attach is either a literal or delimiter + // try to find a suitable parent, otherwise attach the html attributes to the block + if (inline is LiteralInline) + { + while (true) { - while (true) + inline = inline.Parent; + if (!(inline is DelimiterInline)) { - inline = inline.Parent; - if (!(inline is DelimiterInline)) - { - break; - } + break; } } - var objectToAttach = inline is null || inline == processor.Root ? (MarkdownObject)processor.Block! : inline; + } + var objectToAttach = inline is null || inline == processor.Root ? (MarkdownObject)processor.Block! : inline; - // If the current block is a Paragraph, but only the HtmlAttributes is used, - // Try to attach the attributes to the following block - if (objectToAttach is ParagraphBlock paragraph && - paragraph.Inline!.FirstChild is null && - processor.Inline is null && - slice.IsEmptyOrWhitespace()) + // If the current block is a Paragraph, but only the HtmlAttributes is used, + // Try to attach the attributes to the following block + if (objectToAttach is ParagraphBlock paragraph && + paragraph.Inline!.FirstChild is null && + processor.Inline is null && + slice.IsEmptyOrWhitespace()) + { + var parent = paragraph.Parent!; + var indexOfParagraph = parent.IndexOf(paragraph); + if (indexOfParagraph + 1 < parent.Count) { - var parent = paragraph.Parent!; - var indexOfParagraph = parent.IndexOf(paragraph); - if (indexOfParagraph + 1 < parent.Count) - { - objectToAttach = parent[indexOfParagraph + 1]; - // We can remove the paragraph as it is empty - paragraph.RemoveAfterProcessInlines = true; - } + objectToAttach = parent[indexOfParagraph + 1]; + // We can remove the paragraph as it is empty + paragraph.RemoveAfterProcessInlines = true; } + } - var currentHtmlAttributes = objectToAttach.GetAttributes(); - attributes.CopyTo(currentHtmlAttributes, true, false); + var currentHtmlAttributes = objectToAttach.GetAttributes(); + attributes.CopyTo(currentHtmlAttributes, true, false); - // Update the position of the attributes - currentHtmlAttributes.Span.Start = processor.GetSourcePosition(startPosition, out int line, out int column); - currentHtmlAttributes.Line = line; - currentHtmlAttributes.Column = column; - currentHtmlAttributes.Span.End = currentHtmlAttributes.Span.Start + slice.Start - startPosition - 1; + // Update the position of the attributes + currentHtmlAttributes.Span.Start = processor.GetSourcePosition(startPosition, out int line, out int column); + currentHtmlAttributes.Line = line; + currentHtmlAttributes.Column = column; + currentHtmlAttributes.Span.End = currentHtmlAttributes.Span.Start + slice.Start - startPosition - 1; - // We don't set the processor.Inline as we don't want to add attach attributes to a particular entity - return true; - } + // We don't set the processor.Inline as we don't want to add attach attributes to a particular entity + return true; + } + + return false; + } + /// + /// Tries to extra from the current position of a slice an HTML attributes {...} + /// + /// The slice to parse. + /// The output attributes or null if not found or invalid + /// true if parsing the HTML attributes was successful + public static bool TryParse(ref StringSlice slice, [NotNullWhen(true)] out HtmlAttributes? attributes) + { + attributes = null; + if (slice.PeekCharExtra(-1) == '{') + { return false; } - /// - /// Tries to extra from the current position of a slice an HTML attributes {...} - /// - /// The slice to parse. - /// The output attributes or null if not found or invalid - /// true if parsing the HTML attributes was successful - public static bool TryParse(ref StringSlice slice, [NotNullWhen(true)] out HtmlAttributes? attributes) + var line = slice; + + string? id = null; + List? classes = null; + List>? properties = null; + + bool isValid = false; + var c = line.NextChar(); + while (true) { - attributes = null; - if (slice.PeekCharExtra(-1) == '{') + if (c == '}') { - return false; + isValid = true; + line.SkipChar(); // skip } + break; } - var line = slice; - - string? id = null; - List? classes = null; - List>? properties = null; + if (c == '\0') + { + break; + } - bool isValid = false; - var c = line.NextChar(); - while (true) + bool isClass = c == '.'; + if (c == '#' || isClass) { - if (c == '}') + c = line.NextChar(); // Skip # + var start = line.Start; + // Get all non-whitespace characters following a # + // But stop if we found a } or \0 + while (c != '}' && c != '\0' && !c.IsWhitespace()) { - isValid = true; - line.SkipChar(); // skip } - break; + c = line.NextChar(); } - - if (c == '\0') + var end = line.Start - 1; + if (end == start) { break; } - - bool isClass = c == '.'; - if (c == '#' || isClass) + var text = slice.Text.Substring(start, end - start + 1); + if (isClass) { - c = line.NextChar(); // Skip # - var start = line.Start; - // Get all non-whitespace characters following a # - // But stop if we found a } or \0 - while (c != '}' && c != '\0' && !c.IsWhitespace()) - { - c = line.NextChar(); - } - var end = line.Start - 1; - if (end == start) + if (classes is null) { - break; - } - var text = slice.Text.Substring(start, end - start + 1); - if (isClass) - { - if (classes is null) - { - classes = new List(); - } - classes.Add(text); - } - else - { - id = text; + classes = new List(); } - continue; + classes.Add(text); + } + else + { + id = text; } + continue; + } - if (!c.IsWhitespace()) + if (!c.IsWhitespace()) + { + // Parse the attribute name + if (!IsStartAttributeName(c)) { - // Parse the attribute name - if (!IsStartAttributeName(c)) + break; + } + var startName = line.Start; + while (true) + { + c = line.NextChar(); + if (!(c.IsAlphaNumeric() || c == '_' || c == ':' || c == '.' || c == '-')) { break; } - var startName = line.Start; - while (true) - { - c = line.NextChar(); - if (!(c.IsAlphaNumeric() || c == '_' || c == ':' || c == '.' || c == '-')) - { - break; - } - } - var name = slice.Text.Substring(startName, line.Start - startName); + } + var name = slice.Text.Substring(startName, line.Start - startName); - var hasSpace = c.IsSpaceOrTab(); + var hasSpace = c.IsSpaceOrTab(); - // Skip any whitespaces - line.TrimStart(); - c = line.CurrentChar; + // Skip any whitespaces + line.TrimStart(); + c = line.CurrentChar; - // Handle boolean properties that are not followed by = - if ((hasSpace && (c == '.' || c == '#' || IsStartAttributeName(c))) || c == '}') - { - properties ??= new (); - - // Add a null value for the property - properties.Add(new KeyValuePair(name, null)); - continue; - } + // Handle boolean properties that are not followed by = + if ((hasSpace && (c == '.' || c == '#' || IsStartAttributeName(c))) || c == '}') + { + properties ??= new (); + + // Add a null value for the property + properties.Add(new KeyValuePair(name, null)); + continue; + } - // Else we expect a regular property - if (line.CurrentChar != '=') - { - break; - } + // Else we expect a regular property + if (line.CurrentChar != '=') + { + break; + } - // Go to next char, skip any spaces - line.SkipChar(); - line.TrimStart(); + // Go to next char, skip any spaces + line.SkipChar(); + line.TrimStart(); - int startValue = -1; - int endValue = -1; + int startValue = -1; + int endValue = -1; - c = line.CurrentChar; - // Parse a quoted string - if (c == '\'' || c == '"') + c = line.CurrentChar; + // Parse a quoted string + if (c == '\'' || c == '"') + { + char openingStringChar = c; + startValue = line.Start + 1; + while (true) { - char openingStringChar = c; - startValue = line.Start + 1; - while (true) + c = line.NextChar(); + if (c == '\0') + { + return false; + } + if (c == openingStringChar) { - c = line.NextChar(); - if (c == '\0') - { - return false; - } - if (c == openingStringChar) - { - break; - } + break; } - endValue = line.Start - 1; - c = line.NextChar(); // Skip closing opening string char } - else + endValue = line.Start - 1; + c = line.NextChar(); // Skip closing opening string char + } + else + { + // Parse until we match a space or a special html character + startValue = line.Start; + bool valid = false; + while (true) { - // Parse until we match a space or a special html character - startValue = line.Start; - bool valid = false; - while (true) + if (c == '\0') { - if (c == '\0') - { - return false; - } - if (c.IsWhitespace() || c == '}') - { - break; - } - c = line.NextChar(); - valid = true; + return false; } - endValue = line.Start - 1; - if (!valid) + if (c.IsWhitespace() || c == '}') { break; } + c = line.NextChar(); + valid = true; + } + endValue = line.Start - 1; + if (!valid) + { + break; } - - var value = slice.Text.Substring(startValue, endValue - startValue + 1); - - properties ??= new(); - properties.Add(new KeyValuePair(name, value)); - continue; } - c = line.NextChar(); - } - - if (isValid) - { - attributes = new HtmlAttributes() - { - Id = id, - Classes = classes, - Properties = properties - }; + var value = slice.Text.Substring(startValue, endValue - startValue + 1); - // Assign back the current processor of the line to - slice = line; + properties ??= new(); + properties.Add(new KeyValuePair(name, value)); + continue; } - return isValid; + + c = line.NextChar(); } - private static bool IsStartAttributeName(char c) + if (isValid) { - return c.IsAlpha() || c == '_' || c == ':'; + attributes = new HtmlAttributes() + { + Id = id, + Classes = classes, + Properties = properties + }; + + // Assign back the current processor of the line to + slice = line; } + return isValid; + } + + private static bool IsStartAttributeName(char c) + { + return c.IsAlpha() || c == '_' || c == ':'; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Globalization/GlobalizationExtension.cs b/src/Markdig/Extensions/Globalization/GlobalizationExtension.cs index c0e86182f..7f7817b51 100644 --- a/src/Markdig/Extensions/Globalization/GlobalizationExtension.cs +++ b/src/Markdig/Extensions/Globalization/GlobalizationExtension.cs @@ -2,6 +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.Diagnostics; + using Markdig.Extensions.Tables; using Markdig.Extensions.TaskLists; using Markdig.Helpers; @@ -9,117 +11,114 @@ using Markdig.Renderers.Html; using Markdig.Syntax; using Markdig.Syntax.Inlines; -using System.Collections.Generic; -using System.Diagnostics; -namespace Markdig.Extensions.Globalization +namespace Markdig.Extensions.Globalization; + +/// +/// Extension to add support for RTL content. +/// +public class GlobalizationExtension : IMarkdownExtension { - /// - /// Extension to add support for RTL content. - /// - public class GlobalizationExtension : IMarkdownExtension + + public void Setup(MarkdownPipelineBuilder pipeline) { + // Make sure we don't have a delegate twice + pipeline.DocumentProcessed -= Pipeline_DocumentProcessed; + pipeline.DocumentProcessed += Pipeline_DocumentProcessed; + } - public void Setup(MarkdownPipelineBuilder pipeline) + private void Pipeline_DocumentProcessed(MarkdownDocument document) + { + foreach (var node in document.Descendants()) { - // Make sure we don't have a delegate twice - pipeline.DocumentProcessed -= Pipeline_DocumentProcessed; - pipeline.DocumentProcessed += Pipeline_DocumentProcessed; - } + if (node is TableRow || node is TableCell || node is ListItemBlock) + continue; - private void Pipeline_DocumentProcessed(MarkdownDocument document) - { - foreach (var node in document.Descendants()) + if (ShouldBeRightToLeft(node)) { - if (node is TableRow || node is TableCell || node is ListItemBlock) - continue; + var attributes = node.GetAttributes(); + attributes.AddPropertyIfNotExist("dir", "rtl"); - if (ShouldBeRightToLeft(node)) + if (node is Table) { - var attributes = node.GetAttributes(); - attributes.AddPropertyIfNotExist("dir", "rtl"); - - if (node is Table) - { - attributes.AddPropertyIfNotExist("align", "right"); - } + attributes.AddPropertyIfNotExist("align", "right"); } } } + } - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) - { + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { - } + } - private static bool ShouldBeRightToLeft(MarkdownObject item) + private static bool ShouldBeRightToLeft(MarkdownObject item) + { + if (item is IEnumerable container) { - if (item is IEnumerable container) + foreach (var child in container) { - foreach (var child in container) - { - // TaskList items contain an "X", which will cause - // the function to always return false. - if (child is TaskList) - continue; + // TaskList items contain an "X", which will cause + // the function to always return false. + if (child is TaskList) + continue; - return ShouldBeRightToLeft(child); - } - } - else if (item is LeafBlock leaf) - { - return ShouldBeRightToLeft(leaf.Inline!); - } - else if (item is LiteralInline literal) - { - return StartsWithRtlCharacter(literal.Content); + return ShouldBeRightToLeft(child); } + } + else if (item is LeafBlock leaf) + { + return ShouldBeRightToLeft(leaf.Inline!); + } + else if (item is LiteralInline literal) + { + return StartsWithRtlCharacter(literal.Content); + } - foreach (var paragraph in item.Descendants()) + foreach (var paragraph in item.Descendants()) + { + foreach (var inline in paragraph.Inline!) { - foreach (var inline in paragraph.Inline!) + if (inline is LiteralInline literal) { - if (inline is LiteralInline literal) - { - return StartsWithRtlCharacter(literal.Content); - } + return StartsWithRtlCharacter(literal.Content); } } - - return false; } - private static bool StartsWithRtlCharacter(StringSlice slice) + return false; + } + + private static bool StartsWithRtlCharacter(StringSlice slice) + { + for (int i = slice.Start; i <= slice.End; i++) { - for (int i = slice.Start; i <= slice.End; i++) + char c = slice[i]; + if (c < 128) { - char c = slice[i]; - if (c < 128) + if (CharHelper.IsAlpha(c)) { - if (CharHelper.IsAlpha(c)) - { - return false; - } - - continue; - } - - int rune = c; - if (CharHelper.IsHighSurrogate(c) && i < slice.End && CharHelper.IsLowSurrogate(slice[i + 1])) - { - Debug.Assert(char.IsSurrogatePair(c, slice[i + 1])); - rune = char.ConvertToUtf32(c, slice[i + 1]); - i++; + return false; } - if (CharHelper.IsRightToLeft(rune)) - return true; + continue; + } - if (CharHelper.IsLeftToRight(rune)) - return false; + int rune = c; + if (CharHelper.IsHighSurrogate(c) && i < slice.End && CharHelper.IsLowSurrogate(slice[i + 1])) + { + Debug.Assert(char.IsSurrogatePair(c, slice[i + 1])); + rune = char.ConvertToUtf32(c, slice[i + 1]); + i++; } - return false; + if (CharHelper.IsRightToLeft(rune)) + return true; + + if (CharHelper.IsLeftToRight(rune)) + return false; } + + return false; } } diff --git a/src/Markdig/Extensions/Hardlines/SoftlineBreakAsHardlineExtension.cs b/src/Markdig/Extensions/Hardlines/SoftlineBreakAsHardlineExtension.cs index dbc68f1b1..f40c23903 100644 --- a/src/Markdig/Extensions/Hardlines/SoftlineBreakAsHardlineExtension.cs +++ b/src/Markdig/Extensions/Hardlines/SoftlineBreakAsHardlineExtension.cs @@ -1,31 +1,30 @@ -// Copyright (c) Alexandre Mutel. All rights reserved. +// 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.Parsers.Inlines; using Markdig.Renderers; -namespace Markdig.Extensions.Hardlines +namespace Markdig.Extensions.Hardlines; + +/// +/// Extension to generate hardline break for softline breaks. +/// +/// +public class SoftlineBreakAsHardlineExtension : IMarkdownExtension { - /// - /// Extension to generate hardline break for softline breaks. - /// - /// - public class SoftlineBreakAsHardlineExtension : IMarkdownExtension + public void Setup(MarkdownPipelineBuilder pipeline) { - public void Setup(MarkdownPipelineBuilder pipeline) + // Simply modify the LineBreakInlineParser + // TODO: We might want more options (like pandoc) + var parser = pipeline.InlineParsers.Find(); + if (parser != null) { - // Simply modify the LineBreakInlineParser - // TODO: We might want more options (like pandoc) - var parser = pipeline.InlineParsers.Find(); - if (parser != null) - { - parser.EnableSoftAsHard = true; - } + parser.EnableSoftAsHard = true; } + } - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) - { - } + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { } } \ No newline at end of file diff --git a/src/Markdig/Extensions/JiraLinks/JiraLink.cs b/src/Markdig/Extensions/JiraLinks/JiraLink.cs index 33ac3a616..cab87e1c0 100644 --- a/src/Markdig/Extensions/JiraLinks/JiraLink.cs +++ b/src/Markdig/Extensions/JiraLinks/JiraLink.cs @@ -6,27 +6,26 @@ using Markdig.Helpers; using Markdig.Syntax.Inlines; -namespace Markdig.Extensions.JiraLinks +namespace Markdig.Extensions.JiraLinks; + +/// +/// Model for a JIRA link item +/// +[DebuggerDisplay("{ProjectKey}-{Issue}")] +public class JiraLink : LinkInline { - /// - /// Model for a JIRA link item - /// - [DebuggerDisplay("{ProjectKey}-{Issue}")] - public class JiraLink : LinkInline + public JiraLink() { - public JiraLink() - { - IsClosed = true; - } + IsClosed = true; + } - /// - /// JIRA Project Key - /// - public StringSlice ProjectKey { get; set; } + /// + /// JIRA Project Key + /// + public StringSlice ProjectKey { get; set; } - /// - /// JIRA Issue Number - /// - public StringSlice Issue { get; set; } - } + /// + /// JIRA Issue Number + /// + public StringSlice Issue { get; set; } } diff --git a/src/Markdig/Extensions/JiraLinks/JiraLinkExtension.cs b/src/Markdig/Extensions/JiraLinks/JiraLinkExtension.cs index 013a2e8b1..7df8937fa 100644 --- a/src/Markdig/Extensions/JiraLinks/JiraLinkExtension.cs +++ b/src/Markdig/Extensions/JiraLinks/JiraLinkExtension.cs @@ -7,39 +7,38 @@ using Markdig.Renderers.Normalize.Inlines; using Markdig.Renderers.Normalize; -namespace Markdig.Extensions.JiraLinks +namespace Markdig.Extensions.JiraLinks; + +/// +/// Simple inline parser extension for Markdig to find, and +/// automatically add links to JIRA issue numbers. +/// +public class JiraLinkExtension : IMarkdownExtension { - /// - /// Simple inline parser extension for Markdig to find, and - /// automatically add links to JIRA issue numbers. - /// - public class JiraLinkExtension : IMarkdownExtension + private readonly JiraLinkOptions _options; + + public JiraLinkExtension(JiraLinkOptions options) { - private readonly JiraLinkOptions _options; + _options = options; + } - public JiraLinkExtension(JiraLinkOptions options) + public void Setup(MarkdownPipelineBuilder pipeline) + { + if (!pipeline.InlineParsers.Contains()) { - _options = options; + // Insert the parser before the link inline parser + pipeline.InlineParsers.InsertBefore(new JiraLinkInlineParser(_options)); } + } - public void Setup(MarkdownPipelineBuilder pipeline) - { - if (!pipeline.InlineParsers.Contains()) - { - // Insert the parser before the link inline parser - pipeline.InlineParsers.InsertBefore(new JiraLinkInlineParser(_options)); - } - } + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + // No HTML renderer required, since JiraLink type derives from InlineLink (which already has an HTML renderer) - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + if (renderer is NormalizeRenderer normalizeRenderer && !normalizeRenderer.ObjectRenderers.Contains()) { - // No HTML renderer required, since JiraLink type derives from InlineLink (which already has an HTML renderer) - - if (renderer is NormalizeRenderer normalizeRenderer && !normalizeRenderer.ObjectRenderers.Contains()) - { - normalizeRenderer.ObjectRenderers.InsertBefore(new NormalizeJiraLinksRenderer()); - } + normalizeRenderer.ObjectRenderers.InsertBefore(new NormalizeJiraLinksRenderer()); } } - } + diff --git a/src/Markdig/Extensions/JiraLinks/JiraLinkInlineParser.cs b/src/Markdig/Extensions/JiraLinks/JiraLinkInlineParser.cs index e61a1468f..3793c73ef 100644 --- a/src/Markdig/Extensions/JiraLinks/JiraLinkInlineParser.cs +++ b/src/Markdig/Extensions/JiraLinks/JiraLinkInlineParser.cs @@ -2,123 +2,120 @@ // 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 Markdig.Helpers; using Markdig.Parsers; using Markdig.Renderers.Html; using Markdig.Syntax.Inlines; -namespace Markdig.Extensions.JiraLinks +namespace Markdig.Extensions.JiraLinks; + +/// +/// Finds and replaces JIRA links inline +/// +public class JiraLinkInlineParser : InlineParser { - /// - /// Finds and replaces JIRA links inline - /// - public class JiraLinkInlineParser : InlineParser + private readonly JiraLinkOptions _options; + private readonly string _baseUrl; + + public JiraLinkInlineParser(JiraLinkOptions options) { - private readonly JiraLinkOptions _options; - private readonly string _baseUrl; + _options = options ?? throw new ArgumentNullException(nameof(options)); + _baseUrl = _options.GetUrl(); + //look for uppercase chars at the start (for the project key) + OpeningCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".ToCharArray(); + } - public JiraLinkInlineParser(JiraLinkOptions options) + public override bool Match(InlineProcessor processor, ref StringSlice slice) + { + // Allow preceding whitespace or `(` + var pc = slice.PeekCharExtra(-1); + if (!pc.IsWhiteSpaceOrZero() && pc != '(') { - _options = options ?? throw new ArgumentNullException(nameof(options)); - _baseUrl = _options.GetUrl(); - //look for uppercase chars at the start (for the project key) - OpeningCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".ToCharArray(); + return false; } - public override bool Match(InlineProcessor processor, ref StringSlice slice) - { - // Allow preceding whitespace or `(` - var pc = slice.PeekCharExtra(-1); - if (!pc.IsWhiteSpaceOrZero() && pc != '(') - { - return false; - } + var current = slice.CurrentChar; - var current = slice.CurrentChar; + var startKey = slice.Start; + var endKey = slice.Start; - var startKey = slice.Start; - var endKey = slice.Start; - - // the first character of the key can not be a digit. - if (current.IsDigit()) - { - return false; - } + // the first character of the key can not be a digit. + if (current.IsDigit()) + { + return false; + } - // read as many uppercase characters or digits as required - project key - while (current.IsAlphaUpper() || current.IsDigit()) - { - endKey = slice.Start; - current = slice.NextChar(); - } + // read as many uppercase characters or digits as required - project key + while (current.IsAlphaUpper() || current.IsDigit()) + { + endKey = slice.Start; + current = slice.NextChar(); + } - //require a '-' between key and issue number - if (!current.Equals('-')) - { - return false; - } + //require a '-' between key and issue number + if (!current.Equals('-')) + { + return false; + } - current = slice.NextChar(); // skip - + current = slice.NextChar(); // skip - - //read as many numbers as required - issue number - if (!current.IsDigit()) - { - return false; - } + //read as many numbers as required - issue number + if (!current.IsDigit()) + { + return false; + } - var startIssue = slice.Start; - var endIssue = slice.Start; + var startIssue = slice.Start; + var endIssue = slice.Start; - while (current.IsDigit()) - { - endIssue = slice.Start; - current = slice.NextChar(); - } + while (current.IsDigit()) + { + endIssue = slice.Start; + current = slice.NextChar(); + } - if (!current.IsWhiteSpaceOrZero() && current != ')') //can be followed only by a whitespace or `)` - { - return false; - } + if (!current.IsWhiteSpaceOrZero() && current != ')') //can be followed only by a whitespace or `)` + { + return false; + } - var jiraLink = new JiraLink() //create the link at the relevant position - { - Span = - { - Start = processor.GetSourcePosition(slice.Start, out int line, out int column) - }, - Line = line, - Column = column, - Issue = new StringSlice(slice.Text, startIssue, endIssue), - ProjectKey = new StringSlice(slice.Text, startKey, endKey), - }; - jiraLink.Span.End = jiraLink.Span.Start + (endIssue - startKey); - - // Builds the Url - 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.AsSpan()); - builder.Append('-'); - builder.Append(jiraLink.Issue.AsSpan()); - jiraLink.AppendChild(new LiteralInline(builder.ToString())); - - if (_options.OpenInNewWindow) + var jiraLink = new JiraLink() //create the link at the relevant position + { + Span = { - jiraLink.GetAttributes().AddProperty("target", "_blank"); - } + Start = processor.GetSourcePosition(slice.Start, out int line, out int column) + }, + Line = line, + Column = column, + Issue = new StringSlice(slice.Text, startIssue, endIssue), + ProjectKey = new StringSlice(slice.Text, startKey, endKey), + }; + jiraLink.Span.End = jiraLink.Span.Start + (endIssue - startKey); + + // Builds the Url + 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.AsSpan()); + builder.Append('-'); + builder.Append(jiraLink.Issue.AsSpan()); + jiraLink.AppendChild(new LiteralInline(builder.ToString())); + + if (_options.OpenInNewWindow) + { + jiraLink.GetAttributes().AddProperty("target", "_blank"); + } - processor.Inline = jiraLink; + processor.Inline = jiraLink; - return true; - } + return true; } - } diff --git a/src/Markdig/Extensions/JiraLinks/JiraLinkOptions.cs b/src/Markdig/Extensions/JiraLinks/JiraLinkOptions.cs index bc5d8e9e0..1a9e5ce1d 100644 --- a/src/Markdig/Extensions/JiraLinks/JiraLinkOptions.cs +++ b/src/Markdig/Extensions/JiraLinks/JiraLinkOptions.cs @@ -3,47 +3,45 @@ // See the license.txt file in the project root for more information. using Markdig.Helpers; -using System; -namespace Markdig.Extensions.JiraLinks +namespace Markdig.Extensions.JiraLinks; + +/// +/// Available options for replacing JIRA links +/// +public class JiraLinkOptions { /// - /// Available options for replacing JIRA links + /// The base Url (e.g. `https://mycompany.atlassian.net`) /// - public class JiraLinkOptions - { - /// - /// The base Url (e.g. `https://mycompany.atlassian.net`) - /// - public string BaseUrl { get; set; } + public string BaseUrl { get; set; } - /// - /// The base path after the base url (default is `/browse`) - /// - public string BasePath { get; set; } + /// + /// The base path after the base url (default is `/browse`) + /// + public string BasePath { get; set; } - /// - /// Should the link open in a new window when clicked - /// - public bool OpenInNewWindow { get; set; } + /// + /// Should the link open in a new window when clicked + /// + public bool OpenInNewWindow { get; set; } - public JiraLinkOptions(string baseUrl) - { - OpenInNewWindow = true; //default - BaseUrl = baseUrl; - BasePath = "/browse"; - } + public JiraLinkOptions(string baseUrl) + { + OpenInNewWindow = true; //default + BaseUrl = baseUrl; + BasePath = "/browse"; + } - /// - /// Gets the full url composed of the and with no trailing `/` - /// - public virtual string GetUrl() - { - var url = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); - url.Append(BaseUrl.AsSpan().TrimEnd('/')); - url.Append('/'); - url.Append(BasePath.AsSpan().Trim('/')); - return url.ToString(); - } + /// + /// Gets the full url composed of the and with no trailing `/` + /// + public virtual string GetUrl() + { + var url = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); + url.Append(BaseUrl.AsSpan().TrimEnd('/')); + url.Append('/'); + url.Append(BasePath.AsSpan().Trim('/')); + return url.ToString(); } } diff --git a/src/Markdig/Extensions/JiraLinks/NormalizeJiraLinksRenderer.cs b/src/Markdig/Extensions/JiraLinks/NormalizeJiraLinksRenderer.cs index 90c58a4bd..f984776e4 100644 --- a/src/Markdig/Extensions/JiraLinks/NormalizeJiraLinksRenderer.cs +++ b/src/Markdig/Extensions/JiraLinks/NormalizeJiraLinksRenderer.cs @@ -4,15 +4,14 @@ using Markdig.Renderers.Normalize; -namespace Markdig.Extensions.JiraLinks +namespace Markdig.Extensions.JiraLinks; + +public class NormalizeJiraLinksRenderer : NormalizeObjectRenderer { - public class NormalizeJiraLinksRenderer : NormalizeObjectRenderer + protected override void Write(NormalizeRenderer renderer, JiraLink obj) { - protected override void Write(NormalizeRenderer renderer, JiraLink obj) - { - renderer.Write(obj.ProjectKey); - renderer.Write("-"); - renderer.Write(obj.Issue); - } + renderer.Write(obj.ProjectKey); + renderer.Write("-"); + renderer.Write(obj.Issue); } } diff --git a/src/Markdig/Extensions/ListExtras/ListExtraExtension.cs b/src/Markdig/Extensions/ListExtras/ListExtraExtension.cs index cb4426d9d..0550814f6 100644 --- a/src/Markdig/Extensions/ListExtras/ListExtraExtension.cs +++ b/src/Markdig/Extensions/ListExtras/ListExtraExtension.cs @@ -5,25 +5,24 @@ using Markdig.Parsers; using Markdig.Renderers; -namespace Markdig.Extensions.ListExtras +namespace Markdig.Extensions.ListExtras; + +/// +/// Extension for adding new type of list items (a., A., i., I.) +/// +/// +public class ListExtraExtension : IMarkdownExtension { - /// - /// Extension for adding new type of list items (a., A., i., I.) - /// - /// - public class ListExtraExtension : IMarkdownExtension + public void Setup(MarkdownPipelineBuilder pipeline) { - public void Setup(MarkdownPipelineBuilder pipeline) + var parser = pipeline.BlockParsers.Find(); + if (parser != null) { - var parser = pipeline.BlockParsers.Find(); - if (parser != null) - { - parser.ItemParsers.AddIfNotAlready(); - } + parser.ItemParsers.AddIfNotAlready(); } + } - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) - { - } + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { } } \ No newline at end of file diff --git a/src/Markdig/Extensions/ListExtras/ListExtraItemParser.cs b/src/Markdig/Extensions/ListExtras/ListExtraItemParser.cs index 4e8dd4ded..3229e604e 100644 --- a/src/Markdig/Extensions/ListExtras/ListExtraItemParser.cs +++ b/src/Markdig/Extensions/ListExtras/ListExtraItemParser.cs @@ -5,77 +5,74 @@ using Markdig.Helpers; using Markdig.Parsers; -namespace Markdig.Extensions.ListExtras -{ - using System; +namespace Markdig.Extensions.ListExtras; +/// +/// Parser that adds supports for parsing alpha/roman list items (e.g: `a)` or `a.` or `ii.` or `II.`) +/// +/// +/// Note that we don't validate roman numbers. +/// +/// +public class ListExtraItemParser : OrderedListItemParser +{ /// - /// Parser that adds supports for parsing alpha/roman list items (e.g: `a)` or `a.` or `ii.` or `II.`) + /// Initializes a new instance of the class. /// - /// - /// Note that we don't validate roman numbers. - /// - /// - public class ListExtraItemParser : OrderedListItemParser + public ListExtraItemParser() { - /// - /// Initializes a new instance of the class. - /// - public ListExtraItemParser() + OpeningCharacters = new char[('z' - 'a' + 1)*2]; + int index = 0; + for (var c = 'A'; c <= 'Z'; c++) { - OpeningCharacters = new char[('z' - 'a' + 1)*2]; - int index = 0; - for (var c = 'A'; c <= 'Z'; c++) - { - OpeningCharacters[index++] = c; - OpeningCharacters[index++] = (char)(c - 'A' + 'a'); - } + OpeningCharacters[index++] = c; + OpeningCharacters[index++] = (char)(c - 'A' + 'a'); } + } - public override bool TryParse(BlockProcessor state, char pendingBulletType, out ListInfo result) - { - result = new ListInfo(); - - var c = state.CurrentChar; + public override bool TryParse(BlockProcessor state, char pendingBulletType, out ListInfo result) + { + result = new ListInfo(); - var isRomanLow = CharHelper.IsRomanLetterLowerPartial(c); - var isRomanUp = !isRomanLow && CharHelper.IsRomanLetterUpperPartial(c); + var c = state.CurrentChar; - // We allow to parse roman only if we start on a new list or the pending list is already a roman list) - if ((isRomanLow || isRomanUp) && (pendingBulletType == '\0' || pendingBulletType == 'i' || pendingBulletType == 'I')) - { - int startChar = state.Start; - // With a roman, we can have multiple characters - // Note that we don't validate roman numbers - do - { - c = state.NextChar(); - } - while (isRomanLow ? CharHelper.IsRomanLetterLowerPartial(c) : CharHelper.IsRomanLetterUpperPartial(c)); + var isRomanLow = CharHelper.IsRomanLetterLowerPartial(c); + var isRomanUp = !isRomanLow && CharHelper.IsRomanLetterUpperPartial(c); - int orderValue = CharHelper.RomanToArabic(state.Line.Text.AsSpan(startChar, state.Start - startChar)); - result.OrderedStart = CharHelper.SmallNumberToString(orderValue); - result.BulletType = isRomanLow ? 'i' : 'I'; - result.DefaultOrderedStart = isRomanLow ? "i" : "I"; - } - else + // We allow to parse roman only if we start on a new list or the pending list is already a roman list) + if ((isRomanLow || isRomanUp) && (pendingBulletType == '\0' || pendingBulletType == 'i' || pendingBulletType == 'I')) + { + int startChar = state.Start; + // With a roman, we can have multiple characters + // Note that we don't validate roman numbers + do { - // otherwise we expect a regular alpha lettered list with a single character. - var isUpper = c.IsAlphaUpper(); - result.OrderedStart = CharHelper.SmallNumberToString((c | 0x20) - 'a' + 1); - result.BulletType = isUpper ? 'A' : 'a'; - result.DefaultOrderedStart = isUpper ? "A" : "a"; - state.NextChar(); + c = state.NextChar(); } + while (isRomanLow ? CharHelper.IsRomanLetterLowerPartial(c) : CharHelper.IsRomanLetterUpperPartial(c)); - // Finally we expect to always have a delimiter '.' or ')' - if (!TryParseDelimiter(state, out char orderedDelimiter)) - { - return false; - } + int orderValue = CharHelper.RomanToArabic(state.Line.Text.AsSpan(startChar, state.Start - startChar)); + result.OrderedStart = CharHelper.SmallNumberToString(orderValue); + result.BulletType = isRomanLow ? 'i' : 'I'; + result.DefaultOrderedStart = isRomanLow ? "i" : "I"; + } + else + { + // otherwise we expect a regular alpha lettered list with a single character. + var isUpper = c.IsAlphaUpper(); + result.OrderedStart = CharHelper.SmallNumberToString((c | 0x20) - 'a' + 1); + result.BulletType = isUpper ? 'A' : 'a'; + result.DefaultOrderedStart = isUpper ? "A" : "a"; + state.NextChar(); + } - result.OrderedDelimiter = orderedDelimiter; - return true; + // Finally we expect to always have a delimiter '.' or ')' + if (!TryParseDelimiter(state, out char orderedDelimiter)) + { + return false; } + + result.OrderedDelimiter = orderedDelimiter; + return true; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Mathematics/HtmlMathBlockRenderer.cs b/src/Markdig/Extensions/Mathematics/HtmlMathBlockRenderer.cs index c56183f91..af5cbd2b2 100644 --- a/src/Markdig/Extensions/Mathematics/HtmlMathBlockRenderer.cs +++ b/src/Markdig/Extensions/Mathematics/HtmlMathBlockRenderer.cs @@ -5,30 +5,29 @@ using Markdig.Renderers; using Markdig.Renderers.Html; -namespace Markdig.Extensions.Mathematics +namespace Markdig.Extensions.Mathematics; + +/// +/// A HTML renderer for a . +/// +/// +public class HtmlMathBlockRenderer : HtmlObjectRenderer { - /// - /// A HTML renderer for a . - /// - /// - public class HtmlMathBlockRenderer : HtmlObjectRenderer + protected override void Write(HtmlRenderer renderer, MathBlock obj) { - protected override void Write(HtmlRenderer renderer, MathBlock obj) + renderer.EnsureLine(); + if (renderer.EnableHtmlForBlock) { - renderer.EnsureLine(); - if (renderer.EnableHtmlForBlock) - { - renderer.Write(""); - renderer.WriteLine("\\["); - } + renderer.Write(""); + renderer.WriteLine("\\["); + } - renderer.WriteLeafRawLines(obj, true, renderer.EnableHtmlEscape); + renderer.WriteLeafRawLines(obj, true, renderer.EnableHtmlEscape); - if (renderer.EnableHtmlForBlock) - { - renderer.Write("\\]"); - renderer.WriteLine(""); - } + if (renderer.EnableHtmlForBlock) + { + renderer.Write("\\]"); + renderer.WriteLine(""); } } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Mathematics/HtmlMathInlineRenderer.cs b/src/Markdig/Extensions/Mathematics/HtmlMathInlineRenderer.cs index 470f3ce4e..c2afb7a85 100644 --- a/src/Markdig/Extensions/Mathematics/HtmlMathInlineRenderer.cs +++ b/src/Markdig/Extensions/Mathematics/HtmlMathInlineRenderer.cs @@ -5,34 +5,33 @@ using Markdig.Renderers; using Markdig.Renderers.Html; -namespace Markdig.Extensions.Mathematics +namespace Markdig.Extensions.Mathematics; + +/// +/// A HTML renderer for a . +/// +/// +public class HtmlMathInlineRenderer : HtmlObjectRenderer { - /// - /// A HTML renderer for a . - /// - /// - public class HtmlMathInlineRenderer : HtmlObjectRenderer + protected override void Write(HtmlRenderer renderer, MathInline obj) { - protected override void Write(HtmlRenderer renderer, MathInline obj) + if (renderer.EnableHtmlForInline) { - if (renderer.EnableHtmlForInline) - { - renderer.Write("\\("); - } + renderer.Write("\\("); + } - if (renderer.EnableHtmlEscape) - { - renderer.WriteEscape(ref obj.Content); - } - else - { - renderer.Write(ref obj.Content); - } + if (renderer.EnableHtmlEscape) + { + renderer.WriteEscape(ref obj.Content); + } + else + { + renderer.Write(ref obj.Content); + } - if (renderer.EnableHtmlForInline) - { - renderer.Write("\\)"); - } + if (renderer.EnableHtmlForInline) + { + renderer.Write("\\)"); } } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Mathematics/MathBlock.cs b/src/Markdig/Extensions/Mathematics/MathBlock.cs index 1a8c0bbf0..542e1ebba 100644 --- a/src/Markdig/Extensions/Mathematics/MathBlock.cs +++ b/src/Markdig/Extensions/Mathematics/MathBlock.cs @@ -5,20 +5,19 @@ using Markdig.Parsers; using Markdig.Syntax; -namespace Markdig.Extensions.Mathematics +namespace Markdig.Extensions.Mathematics; + +/// +/// A math block. +/// +/// +public class MathBlock : FencedCodeBlock { /// - /// A math block. + /// Initializes a new instance of the class. /// - /// - public class MathBlock : FencedCodeBlock + /// The parser. + public MathBlock(BlockParser parser) : base(parser) { - /// - /// Initializes a new instance of the class. - /// - /// The parser. - public MathBlock(BlockParser parser) : base(parser) - { - } } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Mathematics/MathBlockParser.cs b/src/Markdig/Extensions/Mathematics/MathBlockParser.cs index 060685b48..5b6061b13 100644 --- a/src/Markdig/Extensions/Mathematics/MathBlockParser.cs +++ b/src/Markdig/Extensions/Mathematics/MathBlockParser.cs @@ -7,54 +7,53 @@ using Markdig.Renderers.Html; using Markdig.Syntax; -namespace Markdig.Extensions.Mathematics +namespace Markdig.Extensions.Mathematics; + +/// +/// The block parser for a . +/// +/// +public class MathBlockParser : FencedBlockParserBase { /// - /// The block parser for a . + /// Initializes a new instance of the class. /// - /// - public class MathBlockParser : FencedBlockParserBase + public MathBlockParser() { - /// - /// Initializes a new instance of the class. - /// - public MathBlockParser() - { - OpeningCharacters = new [] {'$'}; - // We expect to match only a $$, no less, no more - MinimumMatchCount = 2; - MaximumMatchCount = 2; + OpeningCharacters = new [] {'$'}; + // We expect to match only a $$, no less, no more + MinimumMatchCount = 2; + MaximumMatchCount = 2; - InfoParser = NoInfoParser; + InfoParser = NoInfoParser; - DefaultClass = "math"; + DefaultClass = "math"; - // We don't need a prefix - InfoPrefix = null; - } + // We don't need a prefix + InfoPrefix = null; + } - public string DefaultClass { get; set; } + public string DefaultClass { get; set; } - protected override MathBlock CreateFencedBlock(BlockProcessor processor) + protected override MathBlock CreateFencedBlock(BlockProcessor processor) + { + var block = new MathBlock(this); + if (DefaultClass != null) { - var block = new MathBlock(this); - if (DefaultClass != null) - { - block.GetAttributes().AddClass(DefaultClass); - } - return block; + block.GetAttributes().AddClass(DefaultClass); } + return block; + } - private static bool NoInfoParser(BlockProcessor state, ref StringSlice line, IFencedBlock fenced, char openingCharacter) + private static bool NoInfoParser(BlockProcessor state, ref StringSlice line, IFencedBlock fenced, char openingCharacter) + { + for (int i = line.Start; i <= line.End; i++) { - for (int i = line.Start; i <= line.End; i++) + if (!line.Text[i].IsSpaceOrTab()) { - if (!line.Text[i].IsSpaceOrTab()) - { - return false; - } + return false; } - return true; } + return true; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Mathematics/MathExtension.cs b/src/Markdig/Extensions/Mathematics/MathExtension.cs index 7f10d1717..1ff41c9a4 100644 --- a/src/Markdig/Extensions/Mathematics/MathExtension.cs +++ b/src/Markdig/Extensions/Mathematics/MathExtension.cs @@ -4,42 +4,41 @@ using Markdig.Renderers; -namespace Markdig.Extensions.Mathematics +namespace Markdig.Extensions.Mathematics; + +/// +/// Extension for adding inline mathematics $...$ +/// +/// +public class MathExtension : IMarkdownExtension { - /// - /// Extension for adding inline mathematics $...$ - /// - /// - public class MathExtension : IMarkdownExtension + public void Setup(MarkdownPipelineBuilder pipeline) { - public void Setup(MarkdownPipelineBuilder pipeline) + // Adds the inline parser + if (!pipeline.InlineParsers.Contains()) { - // Adds the inline parser - if (!pipeline.InlineParsers.Contains()) - { - pipeline.InlineParsers.Insert(0, new MathInlineParser()); - } + pipeline.InlineParsers.Insert(0, new MathInlineParser()); + } - // Adds the block parser - if (!pipeline.BlockParsers.Contains()) - { - // Insert before EmphasisInlineParser to take precedence - pipeline.BlockParsers.Insert(0, new MathBlockParser()); - } + // Adds the block parser + if (!pipeline.BlockParsers.Contains()) + { + // Insert before EmphasisInlineParser to take precedence + pipeline.BlockParsers.Insert(0, new MathBlockParser()); } + } - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + if (renderer is HtmlRenderer htmlRenderer) { - if (renderer is HtmlRenderer htmlRenderer) + if (!htmlRenderer.ObjectRenderers.Contains()) + { + htmlRenderer.ObjectRenderers.Insert(0, new HtmlMathInlineRenderer()); + } + if (!htmlRenderer.ObjectRenderers.Contains()) { - if (!htmlRenderer.ObjectRenderers.Contains()) - { - htmlRenderer.ObjectRenderers.Insert(0, new HtmlMathInlineRenderer()); - } - if (!htmlRenderer.ObjectRenderers.Contains()) - { - htmlRenderer.ObjectRenderers.Insert(0, new HtmlMathBlockRenderer()); - } + htmlRenderer.ObjectRenderers.Insert(0, new HtmlMathBlockRenderer()); } } } diff --git a/src/Markdig/Extensions/Mathematics/MathInline.cs b/src/Markdig/Extensions/Mathematics/MathInline.cs index ac92861d1..ddb0b72f1 100644 --- a/src/Markdig/Extensions/Mathematics/MathInline.cs +++ b/src/Markdig/Extensions/Mathematics/MathInline.cs @@ -5,27 +5,26 @@ using Markdig.Helpers; using Markdig.Syntax.Inlines; -namespace Markdig.Extensions.Mathematics +namespace Markdig.Extensions.Mathematics; + +/// +/// A math inline element. +/// +/// +public class MathInline : LeafInline { /// - /// A math inline element. + /// Gets or sets the delimiter character used by this code inline. /// - /// - public class MathInline : LeafInline - { - /// - /// Gets or sets the delimiter character used by this code inline. - /// - public char Delimiter { get; set; } + public char Delimiter { get; set; } - /// - /// Gets or sets the delimiter count. - /// - public int DelimiterCount { get; set; } + /// + /// Gets or sets the delimiter count. + /// + public int DelimiterCount { get; set; } - /// - /// The content as a . - /// - public StringSlice Content; - } + /// + /// The content as a . + /// + public StringSlice Content; } \ No newline at end of file diff --git a/src/Markdig/Extensions/Mathematics/MathInlineParser.cs b/src/Markdig/Extensions/Mathematics/MathInlineParser.cs index c237dd6a7..13700bfb8 100644 --- a/src/Markdig/Extensions/Mathematics/MathInlineParser.cs +++ b/src/Markdig/Extensions/Mathematics/MathInlineParser.cs @@ -7,172 +7,171 @@ using Markdig.Renderers.Html; using Markdig.Syntax; -namespace Markdig.Extensions.Mathematics +namespace Markdig.Extensions.Mathematics; + +/// +/// An inline parser for . +/// +/// +/// +public class MathInlineParser : InlineParser { /// - /// An inline parser for . + /// Initializes a new instance of the class. /// - /// - /// - public class MathInlineParser : InlineParser + public MathInlineParser() { - /// - /// Initializes a new instance of the class. - /// - public MathInlineParser() + OpeningCharacters = new[] { '$' }; + DefaultClass = "math"; + } + + /// + /// Gets or sets the default class to use when creating a math inline block. + /// + public string DefaultClass { get; set; } + + public override bool Match(InlineProcessor processor, ref StringSlice slice) + { + var match = slice.CurrentChar; + var pc = slice.PeekCharExtra(-1); + if (pc == match) { - OpeningCharacters = new[] { '$' }; - DefaultClass = "math"; + return false; } - /// - /// Gets or sets the default class to use when creating a math inline block. - /// - public string DefaultClass { get; set; } + var startPosition = slice.Start; - public override bool Match(InlineProcessor processor, ref StringSlice slice) + // Match the opened $ or $$ + int openDollars = 1; // we have at least a $ + var c = slice.NextChar(); + if (c == match) { - var match = slice.CurrentChar; - var pc = slice.PeekCharExtra(-1); - if (pc == match) - { - return false; - } + openDollars++; + c = slice.NextChar(); + } - var startPosition = slice.Start; + pc.CheckUnicodeCategory(out bool openPrevIsWhiteSpace, out bool openPrevIsPunctuation); + c.CheckUnicodeCategory(out bool openNextIsWhiteSpace, out _); - // Match the opened $ or $$ - int openDollars = 1; // we have at least a $ - var c = slice.NextChar(); - if (c == match) - { - openDollars++; - c = slice.NextChar(); - } + // Check that opening $/$$ is correct, using the different heuristics than for emphasis delimiters + // If a $/$$ is not preceded by a whitespace or punctuation, this is a not a math block + if ((!openPrevIsWhiteSpace && !openPrevIsPunctuation)) + { + return false; + } - pc.CheckUnicodeCategory(out bool openPrevIsWhiteSpace, out bool openPrevIsPunctuation); - c.CheckUnicodeCategory(out bool openNextIsWhiteSpace, out _); + bool isMatching = false; + int closeDollars = 0; - // Check that opening $/$$ is correct, using the different heuristics than for emphasis delimiters - // If a $/$$ is not preceded by a whitespace or punctuation, this is a not a math block - if ((!openPrevIsWhiteSpace && !openPrevIsPunctuation)) - { - return false; - } + // Eat any leading spaces + while (c.IsSpaceOrTab()) + { + c = slice.NextChar(); + } - bool isMatching = false; - int closeDollars = 0; + var start = slice.Start; + var end = 0; - // Eat any leading spaces - while (c.IsSpaceOrTab()) + pc = match; + var lastWhiteSpace = -1; + while (c != '\0') + { + // Don't allow newline in an inline math expression + if (c == '\r' || c == '\n') { - c = slice.NextChar(); + return false; } - var start = slice.Start; - var end = 0; - - pc = match; - var lastWhiteSpace = -1; - while (c != '\0') + // Don't process sticks if we have a '\' as a previous char + if (pc != '\\') { - // Don't allow newline in an inline math expression - if (c == '\r' || c == '\n') + // Record continuous whitespaces at the end + if (c.IsSpaceOrTab()) { - return false; + if (lastWhiteSpace < 0) + { + lastWhiteSpace = slice.Start; + } } - - // Don't process sticks if we have a '\' as a previous char - if (pc != '\\') + else { - // Record continuous whitespaces at the end - if (c.IsSpaceOrTab()) + bool hasClosingDollars = c == match; + if (hasClosingDollars) { - if (lastWhiteSpace < 0) - { - lastWhiteSpace = slice.Start; - } + closeDollars += slice.CountAndSkipChar(match); + c = slice.CurrentChar; } - else + + if (closeDollars >= openDollars) { - bool hasClosingDollars = c == match; - if (hasClosingDollars) - { - closeDollars += slice.CountAndSkipChar(match); - c = slice.CurrentChar; - } - - if (closeDollars >= openDollars) - { - break; - } - - lastWhiteSpace = -1; - if (hasClosingDollars) - { - pc = match; - continue; - } + break; } - } - if (closeDollars > 0) - { - closeDollars = 0; - } - else - { - pc = c; - c = slice.NextChar(); + lastWhiteSpace = -1; + if (hasClosingDollars) + { + pc = match; + continue; + } } } - if (closeDollars >= openDollars) + if (closeDollars > 0) + { + closeDollars = 0; + } + else { - pc.CheckUnicodeCategory(out bool closePrevIsWhiteSpace, out _); - c.CheckUnicodeCategory(out bool closeNextIsWhiteSpace, out bool closeNextIsPunctuation); + pc = c; + c = slice.NextChar(); + } + } - // A closing $/$$ should be followed by at least a punctuation or a whitespace - // and if the character after an opening $/$$ was a whitespace, it should be - // a whitespace as well for the character preceding the closing of $/$$ - if ((!closeNextIsPunctuation && !closeNextIsWhiteSpace) || (openNextIsWhiteSpace != closePrevIsWhiteSpace)) - { - return false; - } + if (closeDollars >= openDollars) + { + pc.CheckUnicodeCategory(out bool closePrevIsWhiteSpace, out _); + c.CheckUnicodeCategory(out bool closeNextIsWhiteSpace, out bool closeNextIsPunctuation); - if (closePrevIsWhiteSpace && lastWhiteSpace > 0) - { - end = lastWhiteSpace + openDollars - 1; - } - else - { - end = slice.Start - 1; - } + // A closing $/$$ should be followed by at least a punctuation or a whitespace + // and if the character after an opening $/$$ was a whitespace, it should be + // a whitespace as well for the character preceding the closing of $/$$ + if ((!closeNextIsPunctuation && !closeNextIsWhiteSpace) || (openNextIsWhiteSpace != closePrevIsWhiteSpace)) + { + return false; + } - // Create a new MathInline - var inline = new MathInline() - { - Span = new SourceSpan(processor.GetSourcePosition(startPosition, out int line, out int column), processor.GetSourcePosition(slice.End)), - Line = line, - Column = column, - Delimiter = match, - DelimiterCount = openDollars, - Content = slice - }; - inline.Content.Start = start; - // We substract the end to the number of opening $ to keep inside the block the additionals $ - inline.Content.End = end - openDollars; - - // Add the default class if necessary - if (DefaultClass != null) - { - inline.GetAttributes().AddClass(DefaultClass); - } - processor.Inline = inline; - isMatching = true; + if (closePrevIsWhiteSpace && lastWhiteSpace > 0) + { + end = lastWhiteSpace + openDollars - 1; + } + else + { + end = slice.Start - 1; } - return isMatching; + // Create a new MathInline + var inline = new MathInline() + { + Span = new SourceSpan(processor.GetSourcePosition(startPosition, out int line, out int column), processor.GetSourcePosition(slice.End)), + Line = line, + Column = column, + Delimiter = match, + DelimiterCount = openDollars, + Content = slice + }; + inline.Content.Start = start; + // We substract the end to the number of opening $ to keep inside the block the additionals $ + inline.Content.End = end - openDollars; + + // Add the default class if necessary + if (DefaultClass != null) + { + inline.GetAttributes().AddClass(DefaultClass); + } + processor.Inline = inline; + isMatching = true; } + + return isMatching; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/MediaLinks/HostProviderBuilder.cs b/src/Markdig/Extensions/MediaLinks/HostProviderBuilder.cs index 696db9843..a95f5e429 100644 --- a/src/Markdig/Extensions/MediaLinks/HostProviderBuilder.cs +++ b/src/Markdig/Extensions/MediaLinks/HostProviderBuilder.cs @@ -2,146 +2,144 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using Markdig.Helpers; -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; -namespace Markdig.Extensions.MediaLinks +using Markdig.Helpers; + +namespace Markdig.Extensions.MediaLinks; + +public class HostProviderBuilder { - public class HostProviderBuilder + private sealed class DelegateProvider : IHostProvider { - private sealed class DelegateProvider : IHostProvider + public DelegateProvider(string hostPrefix, Func handler, bool allowFullscreen = true, string? className = null) { - public DelegateProvider(string hostPrefix, Func handler, bool allowFullscreen = true, string? className = null) - { - HostPrefix = hostPrefix; - Delegate = handler; - AllowFullScreen = allowFullscreen; - Class = className; - } + HostPrefix = hostPrefix; + Delegate = handler; + AllowFullScreen = allowFullscreen; + Class = className; + } - public string HostPrefix { get; } + public string HostPrefix { get; } - public Func Delegate { get; } + public Func Delegate { get; } - public bool AllowFullScreen { get; } + public bool AllowFullScreen { get; } - public string? Class { get; } + public string? Class { get; } - public bool TryHandle(Uri mediaUri, bool isSchemaRelative, [NotNullWhen(true)] out string? iframeUrl) + public bool TryHandle(Uri mediaUri, bool isSchemaRelative, [NotNullWhen(true)] out string? iframeUrl) + { + if (!mediaUri.Host.StartsWith(HostPrefix, StringComparison.OrdinalIgnoreCase)) { - if (!mediaUri.Host.StartsWith(HostPrefix, StringComparison.OrdinalIgnoreCase)) - { - iframeUrl = null; - return false; - } - iframeUrl = Delegate(mediaUri); - return !string.IsNullOrEmpty(iframeUrl); + iframeUrl = null; + return false; } + iframeUrl = Delegate(mediaUri); + return !string.IsNullOrEmpty(iframeUrl); } + } - /// - /// Create a with delegate handler. - /// - /// Prefix of host that can be handled. - /// Handler that generate iframe url, if uri cannot be handled, it can return . - /// Should the generated iframe has allowfullscreen attribute. - /// "class" attribute of generated iframe. - /// A with delegate handler. - public static IHostProvider Create(string hostPrefix, Func handler, bool allowFullScreen = true, string? iframeClass = null) - { - if (string.IsNullOrEmpty(hostPrefix)) - ThrowHelper.ArgumentException("hostPrefix is null or empty.", nameof(hostPrefix)); - if (handler is null) - ThrowHelper.ArgumentNullException(nameof(handler)); + /// + /// Create a with delegate handler. + /// + /// Prefix of host that can be handled. + /// Handler that generate iframe url, if uri cannot be handled, it can return . + /// Should the generated iframe has allowfullscreen attribute. + /// "class" attribute of generated iframe. + /// A with delegate handler. + public static IHostProvider Create(string hostPrefix, Func handler, bool allowFullScreen = true, string? iframeClass = null) + { + if (string.IsNullOrEmpty(hostPrefix)) + ThrowHelper.ArgumentException("hostPrefix is null or empty.", nameof(hostPrefix)); + if (handler is null) + ThrowHelper.ArgumentNullException(nameof(handler)); - return new DelegateProvider(hostPrefix, handler, allowFullScreen, iframeClass); - } + return new DelegateProvider(hostPrefix, handler, allowFullScreen, iframeClass); + } - internal static Dictionary KnownHosts { get; } - = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - ["YouTube"] = Create("www.youtube.com", YouTube, iframeClass: "youtube"), - ["YouTubeShortened"] = Create("youtu.be", YouTubeShortened, iframeClass: "youtube"), - ["Vimeo"] = Create("vimeo.com", Vimeo, iframeClass: "vimeo"), - ["Yandex"] = Create("music.yandex.ru", Yandex, allowFullScreen: false, iframeClass: "yandex"), - ["Odnoklassniki"] = Create("ok.ru", Odnoklassniki, iframeClass: "odnoklassniki"), - }; + internal static Dictionary KnownHosts { get; } + = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["YouTube"] = Create("www.youtube.com", YouTube, iframeClass: "youtube"), + ["YouTubeShortened"] = Create("youtu.be", YouTubeShortened, iframeClass: "youtube"), + ["Vimeo"] = Create("vimeo.com", Vimeo, iframeClass: "vimeo"), + ["Yandex"] = Create("music.yandex.ru", Yandex, allowFullScreen: false, iframeClass: "yandex"), + ["Odnoklassniki"] = Create("ok.ru", Odnoklassniki, iframeClass: "odnoklassniki"), + }; - #region Known providers + #region Known providers - private static readonly string[] SplitAnd = { "&" }; - private static string[] SplitQuery(Uri uri) - { - var query = uri.Query.Substring(uri.Query.IndexOf('?') + 1); - return query.Split(SplitAnd, StringSplitOptions.RemoveEmptyEntries); - } + private static readonly string[] SplitAnd = { "&" }; + private static string[] SplitQuery(Uri uri) + { + var query = uri.Query.Substring(uri.Query.IndexOf('?') + 1); + return query.Split(SplitAnd, StringSplitOptions.RemoveEmptyEntries); + } - private static string? YouTube(Uri uri) + private static string? YouTube(Uri uri) + { + string uriPath = uri.AbsolutePath; + if (string.Equals(uriPath, "/embed", StringComparison.OrdinalIgnoreCase) || uriPath.StartsWith("/embed/", StringComparison.OrdinalIgnoreCase)) { - string uriPath = uri.AbsolutePath; - if (string.Equals(uriPath, "/embed", StringComparison.OrdinalIgnoreCase) || uriPath.StartsWith("/embed/", StringComparison.OrdinalIgnoreCase)) - { - return uri.ToString(); - } - if (!string.Equals(uriPath, "/watch", StringComparison.OrdinalIgnoreCase) && !uriPath.StartsWith("/watch/", StringComparison.OrdinalIgnoreCase)) - { - return null; - } - var queryParams = SplitQuery(uri); - return BuildYouTubeIframeUrl( - queryParams.FirstOrDefault(p => p.StartsWith("v=", StringComparison.Ordinal))?.Substring(2), - queryParams.FirstOrDefault(p => p.StartsWith("t=", StringComparison.Ordinal))?.Substring(2) - ); + return uri.ToString(); } - - private static string? YouTubeShortened(Uri uri) + if (!string.Equals(uriPath, "/watch", StringComparison.OrdinalIgnoreCase) && !uriPath.StartsWith("/watch/", StringComparison.OrdinalIgnoreCase)) { - return BuildYouTubeIframeUrl( - uri.AbsolutePath.Substring(1), - SplitQuery(uri).FirstOrDefault(p => p.StartsWith("t=", StringComparison.Ordinal))?.Substring(2) - ); + return null; } + var queryParams = SplitQuery(uri); + return BuildYouTubeIframeUrl( + queryParams.FirstOrDefault(p => p.StartsWith("v=", StringComparison.Ordinal))?.Substring(2), + queryParams.FirstOrDefault(p => p.StartsWith("t=", StringComparison.Ordinal))?.Substring(2) + ); + } - private static string? BuildYouTubeIframeUrl(string? videoId, string? startTime) - { - if (string.IsNullOrEmpty(videoId)) - { - return null; - } - string url = $"https://www.youtube.com/embed/{videoId}"; - return string.IsNullOrEmpty(startTime) ? url : $"{url}?start={startTime}"; - } + private static string? YouTubeShortened(Uri uri) + { + return BuildYouTubeIframeUrl( + uri.AbsolutePath.Substring(1), + SplitQuery(uri).FirstOrDefault(p => p.StartsWith("t=", StringComparison.Ordinal))?.Substring(2) + ); + } - private static string? Vimeo(Uri uri) + private static string? BuildYouTubeIframeUrl(string? videoId, string? startTime) + { + if (string.IsNullOrEmpty(videoId)) { - var items = uri.GetComponents(UriComponents.Path, UriFormat.Unescaped).Split('/'); - return items.Length > 0 ? $"https://player.vimeo.com/video/{ items[items.Length - 1] }" : null; + return null; } + string url = $"https://www.youtube.com/embed/{videoId}"; + return string.IsNullOrEmpty(startTime) ? url : $"{url}?start={startTime}"; + } - private static string? Odnoklassniki(Uri uri) - { - var items = uri.GetComponents(UriComponents.Path, UriFormat.Unescaped).Split('/'); - return items.Length > 0 ? $"https://ok.ru/videoembed/{ items[items.Length - 1] }" : null; - } + private static string? Vimeo(Uri uri) + { + var items = uri.GetComponents(UriComponents.Path, UriFormat.Unescaped).Split('/'); + return items.Length > 0 ? $"https://player.vimeo.com/video/{ items[items.Length - 1] }" : null; + } - private static string? Yandex(Uri uri) - { - string[] items = uri.GetComponents(UriComponents.Path, UriFormat.Unescaped).Split('/'); - string? albumKeyword = items.Skip(0).FirstOrDefault(); - string? albumId = items.Skip(1).FirstOrDefault(); - string? trackKeyword = items.Skip(2).FirstOrDefault(); - string? trackId = items.Skip(3).FirstOrDefault(); + private static string? Odnoklassniki(Uri uri) + { + var items = uri.GetComponents(UriComponents.Path, UriFormat.Unescaped).Split('/'); + return items.Length > 0 ? $"https://ok.ru/videoembed/{ items[items.Length - 1] }" : null; + } - if (albumKeyword != "album" || albumId is null || trackKeyword != "track" || trackId is null) - { - return null; - } + private static string? Yandex(Uri uri) + { + string[] items = uri.GetComponents(UriComponents.Path, UriFormat.Unescaped).Split('/'); + string? albumKeyword = items.Skip(0).FirstOrDefault(); + string? albumId = items.Skip(1).FirstOrDefault(); + string? trackKeyword = items.Skip(2).FirstOrDefault(); + string? trackId = items.Skip(3).FirstOrDefault(); - return $"https://music.yandex.ru/iframe/#track/{trackId}/{albumId}/"; + if (albumKeyword != "album" || albumId is null || trackKeyword != "track" || trackId is null) + { + return null; } - #endregion + + return $"https://music.yandex.ru/iframe/#track/{trackId}/{albumId}/"; } + #endregion } diff --git a/src/Markdig/Extensions/MediaLinks/IHostProvider.cs b/src/Markdig/Extensions/MediaLinks/IHostProvider.cs index 2c3fd9ac7..6d13bcbd8 100644 --- a/src/Markdig/Extensions/MediaLinks/IHostProvider.cs +++ b/src/Markdig/Extensions/MediaLinks/IHostProvider.cs @@ -2,36 +2,34 @@ // 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.CodeAnalysis; -namespace Markdig.Extensions.MediaLinks +namespace Markdig.Extensions.MediaLinks; + +/// +/// Provides url for media links. +/// +public interface IHostProvider { /// - /// Provides url for media links. + /// "class" attribute of generated iframe. /// - public interface IHostProvider - { - /// - /// "class" attribute of generated iframe. - /// - string? Class { get; } + string? Class { get; } - /// - /// Generate url for iframe. - /// - /// Input media uri. - /// if is a schema relative uri, i.e. uri starts with "//". - /// Generated url for iframe. - /// - bool TryHandle(Uri mediaUri, bool isSchemaRelative, [NotNullWhen(true)] out string? iframeUrl); + /// + /// Generate url for iframe. + /// + /// Input media uri. + /// if is a schema relative uri, i.e. uri starts with "//". + /// Generated url for iframe. + /// + bool TryHandle(Uri mediaUri, bool isSchemaRelative, [NotNullWhen(true)] out string? iframeUrl); - /// - /// Should the generated iframe has allowfullscreen attribute. - /// - /// - /// Should be false for audio embedding. - /// - bool AllowFullScreen { get; } - } + /// + /// Should the generated iframe has allowfullscreen attribute. + /// + /// + /// Should be false for audio embedding. + /// + bool AllowFullScreen { get; } } diff --git a/src/Markdig/Extensions/MediaLinks/MediaLinkExtension.cs b/src/Markdig/Extensions/MediaLinks/MediaLinkExtension.cs index 0938bb2dc..96c8ec8ee 100644 --- a/src/Markdig/Extensions/MediaLinks/MediaLinkExtension.cs +++ b/src/Markdig/Extensions/MediaLinks/MediaLinkExtension.cs @@ -2,174 +2,172 @@ // 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 Markdig.Renderers; using Markdig.Renderers.Html; using Markdig.Renderers.Html.Inlines; using Markdig.Syntax.Inlines; -namespace Markdig.Extensions.MediaLinks +namespace Markdig.Extensions.MediaLinks; + +/// +/// Extension for extending image Markdown links in case a video or an audio file is linked and output proper link. +/// +/// +public class MediaLinkExtension : IMarkdownExtension { - /// - /// Extension for extending image Markdown links in case a video or an audio file is linked and output proper link. - /// - /// - public class MediaLinkExtension : IMarkdownExtension + public MediaLinkExtension() : this(new MediaOptions()) { - public MediaLinkExtension() : this(new MediaOptions()) - { - } + } - public MediaLinkExtension(MediaOptions? options) - { - Options = options ?? new MediaOptions(); - } + public MediaLinkExtension(MediaOptions? options) + { + Options = options ?? new MediaOptions(); + } - public MediaOptions Options { get; } + public MediaOptions Options { get; } - public void Setup(MarkdownPipelineBuilder pipeline) - { - } + public void Setup(MarkdownPipelineBuilder pipeline) + { + } - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + if (renderer is HtmlRenderer htmlRenderer) { - if (renderer is HtmlRenderer htmlRenderer) + var inlineRenderer = htmlRenderer.ObjectRenderers.FindExact(); + if (inlineRenderer != null) { - var inlineRenderer = htmlRenderer.ObjectRenderers.FindExact(); - if (inlineRenderer != null) - { - inlineRenderer.TryWriters.Remove(TryLinkInlineRenderer); - inlineRenderer.TryWriters.Add(TryLinkInlineRenderer); - } + inlineRenderer.TryWriters.Remove(TryLinkInlineRenderer); + inlineRenderer.TryWriters.Add(TryLinkInlineRenderer); } } + } - private bool TryLinkInlineRenderer(HtmlRenderer renderer, LinkInline linkInline) + private bool TryLinkInlineRenderer(HtmlRenderer renderer, LinkInline linkInline) + { + if (!linkInline.IsImage || linkInline.Url is null) { - if (!linkInline.IsImage || linkInline.Url is null) - { - return false; - } - - bool isSchemaRelative = false; - // Only process absolute Uri - if (!Uri.TryCreate(linkInline.Url, UriKind.RelativeOrAbsolute, out Uri? uri) || !uri.IsAbsoluteUri) - { - // see https://tools.ietf.org/html/rfc3986#section-4.2 - // since relative uri doesn't support many properties, "http" is used as a placeholder here. - if (linkInline.Url.StartsWith("//", StringComparison.Ordinal) && Uri.TryCreate("http:" + linkInline.Url, UriKind.Absolute, out uri)) - { - isSchemaRelative = true; - } - else - { - return false; - } - } + return false; + } - if (TryRenderIframeFromKnownProviders(uri, isSchemaRelative, renderer, linkInline)) + bool isSchemaRelative = false; + // Only process absolute Uri + if (!Uri.TryCreate(linkInline.Url, UriKind.RelativeOrAbsolute, out Uri? uri) || !uri.IsAbsoluteUri) + { + // see https://tools.ietf.org/html/rfc3986#section-4.2 + // since relative uri doesn't support many properties, "http" is used as a placeholder here. + if (linkInline.Url.StartsWith("//", StringComparison.Ordinal) && Uri.TryCreate("http:" + linkInline.Url, UriKind.Absolute, out uri)) { - return true; + isSchemaRelative = true; } - - if (TryGuessAudioVideoFile(uri, isSchemaRelative, renderer, linkInline)) + else { - return true; + return false; } + } - return false; + if (TryRenderIframeFromKnownProviders(uri, isSchemaRelative, renderer, linkInline)) + { + return true; } - private static HtmlAttributes GetHtmlAttributes(LinkInline linkInline) + if (TryGuessAudioVideoFile(uri, isSchemaRelative, renderer, linkInline)) { - var htmlAttributes = new HtmlAttributes(); - var fromAttributes = linkInline.TryGetAttributes(); - if (fromAttributes != null) - { - fromAttributes.CopyTo(htmlAttributes, false, false); - } + return true; + } - return htmlAttributes; + return false; + } + + private static HtmlAttributes GetHtmlAttributes(LinkInline linkInline) + { + var htmlAttributes = new HtmlAttributes(); + var fromAttributes = linkInline.TryGetAttributes(); + if (fromAttributes != null) + { + fromAttributes.CopyTo(htmlAttributes, false, false); } - private bool TryGuessAudioVideoFile(Uri uri, bool isSchemaRelative, HtmlRenderer renderer, LinkInline linkInline) + return htmlAttributes; + } + + private bool TryGuessAudioVideoFile(Uri uri, bool isSchemaRelative, HtmlRenderer renderer, LinkInline linkInline) + { + var path = uri.GetComponents(UriComponents.Path, UriFormat.Unescaped); + // Otherwise try to detect if we have an audio/video from the file extension + var lastDot = path.LastIndexOf('.'); + if (lastDot >= 0 && + Options.ExtensionToMimeType.TryGetValue(path.Substring(lastDot), out string? mimeType)) { - var path = uri.GetComponents(UriComponents.Path, UriFormat.Unescaped); - // Otherwise try to detect if we have an audio/video from the file extension - var lastDot = path.LastIndexOf('.'); - if (lastDot >= 0 && - Options.ExtensionToMimeType.TryGetValue(path.Substring(lastDot), out string? mimeType)) - { - var htmlAttributes = GetHtmlAttributes(linkInline); - var isAudio = mimeType.StartsWith("audio", StringComparison.Ordinal); - var tagType = isAudio ? "audio" : "video"; + var htmlAttributes = GetHtmlAttributes(linkInline); + var isAudio = mimeType.StartsWith("audio", StringComparison.Ordinal); + var tagType = isAudio ? "audio" : "video"; - renderer.Write($"<{tagType}"); - htmlAttributes.AddPropertyIfNotExist("width", Options.Width); - if (!isAudio) - { - htmlAttributes.AddPropertyIfNotExist("height", Options.Height); - } - if (Options.AddControlsProperty) - { - htmlAttributes.AddPropertyIfNotExist("controls", null); - } + renderer.Write($"<{tagType}"); + htmlAttributes.AddPropertyIfNotExist("width", Options.Width); + if (!isAudio) + { + htmlAttributes.AddPropertyIfNotExist("height", Options.Height); + } + if (Options.AddControlsProperty) + { + htmlAttributes.AddPropertyIfNotExist("controls", null); + } - if (!string.IsNullOrEmpty(Options.Class)) - htmlAttributes.AddClass(Options.Class); + if (!string.IsNullOrEmpty(Options.Class)) + htmlAttributes.AddClass(Options.Class); - renderer.WriteAttributes(htmlAttributes); + renderer.WriteAttributes(htmlAttributes); - renderer.Write($">"); + renderer.Write($">"); - return true; - } - return false; + return true; } + return false; + } - private bool TryRenderIframeFromKnownProviders(Uri uri, bool isSchemaRelative, HtmlRenderer renderer, LinkInline linkInline) + private bool TryRenderIframeFromKnownProviders(Uri uri, bool isSchemaRelative, HtmlRenderer renderer, LinkInline linkInline) + { + IHostProvider? foundProvider = null; + string? iframeUrl = null; + foreach (var provider in Options.Hosts) { - IHostProvider? foundProvider = null; - string? iframeUrl = null; - foreach (var provider in Options.Hosts) - { - if (!provider.TryHandle(uri, isSchemaRelative, out iframeUrl)) - continue; - foundProvider = provider; - break; - } + if (!provider.TryHandle(uri, isSchemaRelative, out iframeUrl)) + continue; + foundProvider = provider; + break; + } - if (foundProvider is null) - { - return false; - } + if (foundProvider is null) + { + return false; + } - var htmlAttributes = GetHtmlAttributes(linkInline); - renderer.Write(""); - - return true; + htmlAttributes.AddPropertyIfNotExist("frameborder", "0"); + if (foundProvider.AllowFullScreen) + { + htmlAttributes.AddPropertyIfNotExist("allowfullscreen", null); } + renderer.WriteAttributes(htmlAttributes); + renderer.Write(">"); + + return true; } } diff --git a/src/Markdig/Extensions/MediaLinks/MediaOptions.cs b/src/Markdig/Extensions/MediaLinks/MediaOptions.cs index e705cef3f..de682e69d 100644 --- a/src/Markdig/Extensions/MediaLinks/MediaOptions.cs +++ b/src/Markdig/Extensions/MediaLinks/MediaOptions.cs @@ -2,101 +2,97 @@ // 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.Collections.Generic; +namespace Markdig.Extensions.MediaLinks; -namespace Markdig.Extensions.MediaLinks +/// +/// Options for the . +/// +public class MediaOptions { - /// - /// Options for the . - /// - public class MediaOptions + public MediaOptions() { - public MediaOptions() + Width = "500"; + Height = "281"; + AddControlsProperty = true; + Class = ""; + ExtensionToMimeType = new Dictionary(StringComparer.OrdinalIgnoreCase) { - Width = "500"; - Height = "281"; - AddControlsProperty = true; - Class = ""; - ExtensionToMimeType = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - {".3gp", "video/3gpp"}, - {".3g2", "video/3gpp2"}, - {".avi", "video/x-msvideo"}, - {".uvh", "video/vnd.dece.hd"}, - {".uvm", "video/vnd.dece.mobile"}, - {".uvu", "video/vnd.uvvu.mp4"}, - {".uvp", "video/vnd.dece.pd"}, - {".uvs", "video/vnd.dece.sd"}, - {".uvv", "video/vnd.dece.video"}, - {".fvt", "video/vnd.fvt"}, - {".f4v", "video/x-f4v"}, - {".flv", "video/x-flv"}, - {".fli", "video/x-fli"}, - {".h261", "video/h261"}, - {".h263", "video/h263"}, - {".h264", "video/h264"}, - {".jpm", "video/jpm"}, - {".jpgv", "video/jpeg"}, - {".m4v", "video/x-m4v"}, - {".asf", "video/x-ms-asf"}, - {".pyv", "video/vnd.ms-playready.media.pyv"}, - {".wm", "video/x-ms-wm"}, - {".wmx", "video/x-ms-wmx"}, - {".wmv", "video/x-ms-wmv"}, - {".wvx", "video/x-ms-wvx"}, - {".mj2", "video/mj2"}, - {".mxu", "video/vnd.mpegurl"}, - {".mpeg", "video/mpeg"}, - {".mp4", "video/mp4"}, - {".ogv", "video/ogg"}, - {".webm", "video/webm"}, - {".qt", "video/quicktime"}, - {".movie", "video/x-sgi-movie"}, - {".viv", "video/vnd.vivo"}, + {".3gp", "video/3gpp"}, + {".3g2", "video/3gpp2"}, + {".avi", "video/x-msvideo"}, + {".uvh", "video/vnd.dece.hd"}, + {".uvm", "video/vnd.dece.mobile"}, + {".uvu", "video/vnd.uvvu.mp4"}, + {".uvp", "video/vnd.dece.pd"}, + {".uvs", "video/vnd.dece.sd"}, + {".uvv", "video/vnd.dece.video"}, + {".fvt", "video/vnd.fvt"}, + {".f4v", "video/x-f4v"}, + {".flv", "video/x-flv"}, + {".fli", "video/x-fli"}, + {".h261", "video/h261"}, + {".h263", "video/h263"}, + {".h264", "video/h264"}, + {".jpm", "video/jpm"}, + {".jpgv", "video/jpeg"}, + {".m4v", "video/x-m4v"}, + {".asf", "video/x-ms-asf"}, + {".pyv", "video/vnd.ms-playready.media.pyv"}, + {".wm", "video/x-ms-wm"}, + {".wmx", "video/x-ms-wmx"}, + {".wmv", "video/x-ms-wmv"}, + {".wvx", "video/x-ms-wvx"}, + {".mj2", "video/mj2"}, + {".mxu", "video/vnd.mpegurl"}, + {".mpeg", "video/mpeg"}, + {".mp4", "video/mp4"}, + {".ogv", "video/ogg"}, + {".webm", "video/webm"}, + {".qt", "video/quicktime"}, + {".movie", "video/x-sgi-movie"}, + {".viv", "video/vnd.vivo"}, - {".adp", "audio/adpcm"}, - {".aac", "audio/x-aac"}, - {".aif", "audio/x-aiff"}, - {".uva", "audio/vnd.dece.audio"}, - {".eol", "audio/vnd.digital-winds"}, - {".dra", "audio/vnd.dra"}, - {".dts", "audio/vnd.dts"}, - {".dtshd", "audio/vnd.dts.hd"}, - {".rip", "audio/vnd.rip"}, - {".lvp", "audio/vnd.lucent.voice"}, - {".m3u", "audio/x-mpegurl"}, - {".pya", "audio/vnd.ms-playready.media.pya"}, - {".wma", "audio/x-ms-wma"}, - {".wax", "audio/x-ms-wax"}, - {".mid", "audio/midi"}, - {".mp3", "audio/mpeg"}, - {".mpga", "audio/mpeg"}, - {".mp4a", "audio/mp4"}, - {".ecelp4800", "audio/vnd.nuera.ecelp4800"}, - {".ecelp7470", "audio/vnd.nuera.ecelp7470"}, - {".ecelp9600", "audio/vnd.nuera.ecelp9600"}, - {".oga", "audio/ogg"}, - {".ogg", "audio/ogg"}, - {".weba", "audio/webm"}, - {".ram", "audio/x-pn-realaudio"}, - {".rmp", "audio/x-pn-realaudio-plugin"}, - {".au", "audio/basic"}, - {".wav", "audio/x-wav"}, - }; - Hosts = new List(HostProviderBuilder.KnownHosts.Values); - } + {".adp", "audio/adpcm"}, + {".aac", "audio/x-aac"}, + {".aif", "audio/x-aiff"}, + {".uva", "audio/vnd.dece.audio"}, + {".eol", "audio/vnd.digital-winds"}, + {".dra", "audio/vnd.dra"}, + {".dts", "audio/vnd.dts"}, + {".dtshd", "audio/vnd.dts.hd"}, + {".rip", "audio/vnd.rip"}, + {".lvp", "audio/vnd.lucent.voice"}, + {".m3u", "audio/x-mpegurl"}, + {".pya", "audio/vnd.ms-playready.media.pya"}, + {".wma", "audio/x-ms-wma"}, + {".wax", "audio/x-ms-wax"}, + {".mid", "audio/midi"}, + {".mp3", "audio/mpeg"}, + {".mpga", "audio/mpeg"}, + {".mp4a", "audio/mp4"}, + {".ecelp4800", "audio/vnd.nuera.ecelp4800"}, + {".ecelp7470", "audio/vnd.nuera.ecelp7470"}, + {".ecelp9600", "audio/vnd.nuera.ecelp9600"}, + {".oga", "audio/ogg"}, + {".ogg", "audio/ogg"}, + {".weba", "audio/webm"}, + {".ram", "audio/x-pn-realaudio"}, + {".rmp", "audio/x-pn-realaudio-plugin"}, + {".au", "audio/basic"}, + {".wav", "audio/x-wav"}, + }; + Hosts = new List(HostProviderBuilder.KnownHosts.Values); + } - public string Width { get; set; } + public string Width { get; set; } - public string Height { get; set; } + public string Height { get; set; } - public bool AddControlsProperty { get; set; } + public bool AddControlsProperty { get; set; } - public string Class { get; set; } + public string Class { get; set; } - public Dictionary ExtensionToMimeType { get; } + public Dictionary ExtensionToMimeType { get; } - public List Hosts { get; } - } + public List Hosts { get; } } diff --git a/src/Markdig/Extensions/NoRefLinks/NoFollowLinksExtension.cs b/src/Markdig/Extensions/NoRefLinks/NoFollowLinksExtension.cs index 13bc93f75..9c7c1b7b2 100644 --- a/src/Markdig/Extensions/NoRefLinks/NoFollowLinksExtension.cs +++ b/src/Markdig/Extensions/NoRefLinks/NoFollowLinksExtension.cs @@ -4,30 +4,29 @@ using Markdig.Extensions.ReferralLinks; using Markdig.Renderers; -using System; -namespace Markdig.Extensions.NoRefLinks +namespace Markdig.Extensions.NoRefLinks; + +/// +/// Extension to automatically render rel=nofollow to all links in an HTML output. +/// +[Obsolete("Use ReferralLinksExtension class instead")] +public class NoFollowLinksExtension : IMarkdownExtension { - /// - /// Extension to automatically render rel=nofollow to all links in an HTML output. - /// - [Obsolete("Use ReferralLinksExtension class instead")] - public class NoFollowLinksExtension : IMarkdownExtension + private ReferralLinksExtension _referralLinksExtension; + + public NoFollowLinksExtension() { - private ReferralLinksExtension _referralLinksExtension; + _referralLinksExtension = new ReferralLinksExtension(new[] { "nofollow" }); + } - public NoFollowLinksExtension() - { - _referralLinksExtension = new ReferralLinksExtension(new[] { "nofollow" }); - } - public void Setup(MarkdownPipelineBuilder pipeline) - { - _referralLinksExtension.Setup(pipeline); - } + public void Setup(MarkdownPipelineBuilder pipeline) + { + _referralLinksExtension.Setup(pipeline); + } - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) - { - _referralLinksExtension.Setup(pipeline, renderer); - } + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + _referralLinksExtension.Setup(pipeline, renderer); } } diff --git a/src/Markdig/Extensions/NonAsciiNoEscape/NonAsciiNoEscapeExtension.cs b/src/Markdig/Extensions/NonAsciiNoEscape/NonAsciiNoEscapeExtension.cs index 2e4bd6748..84e7fcb6f 100644 --- a/src/Markdig/Extensions/NonAsciiNoEscape/NonAsciiNoEscapeExtension.cs +++ b/src/Markdig/Extensions/NonAsciiNoEscape/NonAsciiNoEscapeExtension.cs @@ -4,23 +4,22 @@ using Markdig.Renderers; -namespace Markdig.Extensions.NonAsciiNoEscape +namespace Markdig.Extensions.NonAsciiNoEscape; + +/// +/// Extension that will disable URI escape with % characters for non-US-ASCII characters in order to workaround a bug under IE/Edge with local file links containing non US-ASCII chars. DO NOT USE OTHERWISE. +/// +public class NonAsciiNoEscapeExtension : IMarkdownExtension { - /// - /// Extension that will disable URI escape with % characters for non-US-ASCII characters in order to workaround a bug under IE/Edge with local file links containing non US-ASCII chars. DO NOT USE OTHERWISE. - /// - public class NonAsciiNoEscapeExtension : IMarkdownExtension + public void Setup(MarkdownPipelineBuilder pipeline) { - public void Setup(MarkdownPipelineBuilder pipeline) - { - } + } - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + if (renderer is HtmlRenderer htmlRenderer) { - if (renderer is HtmlRenderer htmlRenderer) - { - htmlRenderer.UseNonAsciiNoEscape = true; - } + htmlRenderer.UseNonAsciiNoEscape = true; } } } \ No newline at end of file diff --git a/src/Markdig/Extensions/PragmaLines/PragmaLineExtension.cs b/src/Markdig/Extensions/PragmaLines/PragmaLineExtension.cs index 2fa46f758..beac5f1dd 100644 --- a/src/Markdig/Extensions/PragmaLines/PragmaLineExtension.cs +++ b/src/Markdig/Extensions/PragmaLines/PragmaLineExtension.cs @@ -8,70 +8,69 @@ using Markdig.Syntax; using Markdig.Syntax.Inlines; -namespace Markdig.Extensions.PragmaLines +namespace Markdig.Extensions.PragmaLines; + +/// +/// Extension to a span for each line containing the original line id (using id = pragma-line#line_number_zero_based) +/// +/// +public class PragmaLineExtension : IMarkdownExtension { - /// - /// Extension to a span for each line containing the original line id (using id = pragma-line#line_number_zero_based) - /// - /// - public class PragmaLineExtension : IMarkdownExtension + public void Setup(MarkdownPipelineBuilder pipeline) { - public void Setup(MarkdownPipelineBuilder pipeline) - { - pipeline.DocumentProcessed -= PipelineOnDocumentProcessed; - pipeline.DocumentProcessed += PipelineOnDocumentProcessed; - } + pipeline.DocumentProcessed -= PipelineOnDocumentProcessed; + pipeline.DocumentProcessed += PipelineOnDocumentProcessed; + } - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) - { - } + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + } + + private static void PipelineOnDocumentProcessed(MarkdownDocument document) + { + int index = 0; + AddPragmas(document, ref index); + } - private static void PipelineOnDocumentProcessed(MarkdownDocument document) + private static void AddPragmas(Block block, ref int index) + { + var attribute = block.GetAttributes(); + var pragmaId = GetPragmaId(block); + if ( attribute.Id is null) { - int index = 0; - AddPragmas(document, ref index); + attribute.Id = pragmaId; } - - private static void AddPragmas(Block block, ref int index) + else if (block.Parent != null) { - var attribute = block.GetAttributes(); - var pragmaId = GetPragmaId(block); - if ( attribute.Id is null) + var heading = block as HeadingBlock; + + // If we have a heading, we will try to add the tag inside it + // otherwise we will add it just before + var tag = $""; + if (heading?.Inline?.FirstChild != null) { - attribute.Id = pragmaId; + heading.Inline.FirstChild.InsertBefore(new HtmlInline(tag)); } - else if (block.Parent != null) + else { - var heading = block as HeadingBlock; - - // If we have a heading, we will try to add the tag inside it - // otherwise we will add it just before - var tag = $""; - if (heading?.Inline?.FirstChild != null) - { - heading.Inline.FirstChild.InsertBefore(new HtmlInline(tag)); - } - else - { - block.Parent.Insert(index, new HtmlBlock(null) { Lines = new StringLineGroup(tag) }); - index++; - } + block.Parent.Insert(index, new HtmlBlock(null) { Lines = new StringLineGroup(tag) }); + index++; } + } - var container = block as ContainerBlock; - if (container != null) + var container = block as ContainerBlock; + if (container != null) + { + for (int i = 0; i < container.Count; i++) { - for (int i = 0; i < container.Count; i++) - { - var subBlock = container[i]; - AddPragmas(subBlock, ref i); - } + var subBlock = container[i]; + AddPragmas(subBlock, ref i); } } + } - private static string GetPragmaId(Block block) - { - return $"pragma-line-{block.Line}"; - } + private static string GetPragmaId(Block block) + { + return $"pragma-line-{block.Line}"; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/ReferralLinks/ReferralLinksExtension.cs b/src/Markdig/Extensions/ReferralLinks/ReferralLinksExtension.cs index 089f37ed7..28a2fb3ea 100644 --- a/src/Markdig/Extensions/ReferralLinks/ReferralLinksExtension.cs +++ b/src/Markdig/Extensions/ReferralLinks/ReferralLinksExtension.cs @@ -2,42 +2,40 @@ // 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.Collections.Generic; using System.Linq; + using Markdig.Renderers; using Markdig.Renderers.Html.Inlines; -namespace Markdig.Extensions.ReferralLinks +namespace Markdig.Extensions.ReferralLinks; + +public class ReferralLinksExtension : IMarkdownExtension { - public class ReferralLinksExtension : IMarkdownExtension + public ReferralLinksExtension(string[] rels) { - public ReferralLinksExtension(string[] rels) - { - Rels = rels?.ToList() ?? throw new ArgumentNullException(nameof(rels)); - } + Rels = rels?.ToList() ?? throw new ArgumentNullException(nameof(rels)); + } - public List Rels { get; } + public List Rels { get; } + + public void Setup(MarkdownPipelineBuilder pipeline) + { + } + + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + string relString = string.Join(" ", Rels.Where(r => !string.IsNullOrEmpty(r))); - public void Setup(MarkdownPipelineBuilder pipeline) + var linkRenderer = renderer.ObjectRenderers.Find(); + if (linkRenderer != null) { + linkRenderer.Rel = relString; } - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + var autolinkRenderer = renderer.ObjectRenderers.Find(); + if (autolinkRenderer != null) { - string relString = string.Join(" ", Rels.Where(r => !string.IsNullOrEmpty(r))); - - var linkRenderer = renderer.ObjectRenderers.Find(); - if (linkRenderer != null) - { - linkRenderer.Rel = relString; - } - - var autolinkRenderer = renderer.ObjectRenderers.Find(); - if (autolinkRenderer != null) - { - autolinkRenderer.Rel = relString; - } + autolinkRenderer.Rel = relString; } } } diff --git a/src/Markdig/Extensions/SelfPipeline/SelfPipelineExtension.cs b/src/Markdig/Extensions/SelfPipeline/SelfPipelineExtension.cs index 895e6bcbc..8bf043572 100644 --- a/src/Markdig/Extensions/SelfPipeline/SelfPipelineExtension.cs +++ b/src/Markdig/Extensions/SelfPipeline/SelfPipelineExtension.cs @@ -2,99 +2,97 @@ // 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 Markdig.Helpers; using Markdig.Renderers; -namespace Markdig.Extensions.SelfPipeline +namespace Markdig.Extensions.SelfPipeline; + +/// +/// Extension to enable SelfPipeline, to configure a Markdown parsing/convertion to HTML automatically +/// from an embedded special tag in the input text <!--markdig:extensions--> where extensions is a string +/// that specifies the extensions to use for the pipeline as exposed by extension method +/// on the . This extension will invalidate all other extensions and will override them. +/// +public sealed class SelfPipelineExtension : IMarkdownExtension { + public const string DefaultTag = "markdig"; + /// - /// Extension to enable SelfPipeline, to configure a Markdown parsing/convertion to HTML automatically - /// from an embedded special tag in the input text <!--markdig:extensions--> where extensions is a string - /// that specifies the extensions to use for the pipeline as exposed by extension method - /// on the . This extension will invalidate all other extensions and will override them. + /// Initializes a new instance of the class. /// - public sealed class SelfPipelineExtension : IMarkdownExtension + /// The matching start tag. + /// The default extensions. + /// Tag cannot contain angle brackets + public SelfPipelineExtension(string? tag = null, string? defaultExtensions = null) { - public const string DefaultTag = "markdig"; - - /// - /// Initializes a new instance of the class. - /// - /// The matching start tag. - /// The default extensions. - /// Tag cannot contain angle brackets - public SelfPipelineExtension(string? tag = null, string? defaultExtensions = null) + tag = tag?.Trim(); + tag = string.IsNullOrEmpty(tag) ? DefaultTag : tag; + if (tag.AsSpan().IndexOfAny('<', '>') >= 0) { - tag = tag?.Trim(); - tag = string.IsNullOrEmpty(tag) ? DefaultTag : tag; - if (tag.AsSpan().IndexOfAny('<', '>') >= 0) - { - ThrowHelper.ArgumentException("Tag cannot contain `<` or `>` characters", nameof(tag)); - } + ThrowHelper.ArgumentException("Tag cannot contain `<` or `>` characters", nameof(tag)); + } - if (defaultExtensions != null) - { - // Check that this default pipeline is supported - // Will throw an ArgumentInvalidException if not - new MarkdownPipelineBuilder().Configure(defaultExtensions); - } - DefaultExtensions = defaultExtensions; - SelfPipelineHintTagStart = "", optionStart, StringComparison.OrdinalIgnoreCase); + if (endOfTag >= 0) { - var optionStart = indexOfSelfPipeline + SelfPipelineHintTagStart.Length; - var endOfTag = inputText.IndexOf("-->", optionStart, StringComparison.OrdinalIgnoreCase); - if (endOfTag >= 0) - { - defaultConfig = inputText.Substring(optionStart, endOfTag - optionStart).Trim(); - } + defaultConfig = inputText.Substring(optionStart, endOfTag - optionStart).Trim(); } + } - if (defaultConfig is { Length: > 0 }) - { - builder.Configure(defaultConfig); - } - return builder.Build(); + if (defaultConfig is { Length: > 0 }) + { + builder.Configure(defaultConfig); } + return builder.Build(); } } \ No newline at end of file diff --git a/src/Markdig/Extensions/SmartyPants/HtmlSmartyPantRenderer.cs b/src/Markdig/Extensions/SmartyPants/HtmlSmartyPantRenderer.cs index 34a1a3999..009805778 100644 --- a/src/Markdig/Extensions/SmartyPants/HtmlSmartyPantRenderer.cs +++ b/src/Markdig/Extensions/SmartyPants/HtmlSmartyPantRenderer.cs @@ -2,39 +2,37 @@ // 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 Markdig.Renderers; using Markdig.Renderers.Html; -namespace Markdig.Extensions.SmartyPants +namespace Markdig.Extensions.SmartyPants; + +/// +/// A HTML renderer for a . +/// +/// +public class HtmlSmartyPantRenderer : HtmlObjectRenderer { + private static readonly SmartyPantOptions DefaultOptions = new SmartyPantOptions(); + + private readonly SmartyPantOptions options; + /// - /// A HTML renderer for a . + /// Initializes a new instance of the class. /// - /// - public class HtmlSmartyPantRenderer : HtmlObjectRenderer + /// The options. + /// + public HtmlSmartyPantRenderer(SmartyPantOptions? options) { - private static readonly SmartyPantOptions DefaultOptions = new SmartyPantOptions(); - - private readonly SmartyPantOptions options; - - /// - /// Initializes a new instance of the class. - /// - /// The options. - /// - public HtmlSmartyPantRenderer(SmartyPantOptions? options) - { - this.options = options ?? throw new ArgumentNullException(nameof(options)); - } + this.options = options ?? throw new ArgumentNullException(nameof(options)); + } - protected override void Write(HtmlRenderer renderer, SmartyPant obj) + protected override void Write(HtmlRenderer renderer, SmartyPant obj) + { + if (!options.Mapping.TryGetValue(obj.Type, out string? text)) { - if (!options.Mapping.TryGetValue(obj.Type, out string? text)) - { - DefaultOptions.Mapping.TryGetValue(obj.Type, out text); - } - renderer.Write(text); + DefaultOptions.Mapping.TryGetValue(obj.Type, out text); } + renderer.Write(text); } } \ No newline at end of file diff --git a/src/Markdig/Extensions/SmartyPants/SmartyPant.cs b/src/Markdig/Extensions/SmartyPants/SmartyPant.cs index 4f7607305..e7a73a9e4 100644 --- a/src/Markdig/Extensions/SmartyPants/SmartyPant.cs +++ b/src/Markdig/Extensions/SmartyPants/SmartyPant.cs @@ -5,56 +5,55 @@ using System.Diagnostics; using Markdig.Syntax.Inlines; -namespace Markdig.Extensions.SmartyPants +namespace Markdig.Extensions.SmartyPants; + +/// +/// An inline for SmartyPant. +/// +[DebuggerDisplay("SmartyPant {ToString()}")] +public class SmartyPant : LeafInline { + public char OpeningCharacter { get; set; } + + public SmartyPantType Type { get; set; } + /// - /// An inline for SmartyPant. + /// Converts this instance to a literal text. /// - [DebuggerDisplay("SmartyPant {ToString()}")] - public class SmartyPant : LeafInline + /// + public override string ToString() { - public char OpeningCharacter { get; set; } - - public SmartyPantType Type { get; set; } - - /// - /// Converts this instance to a literal text. - /// - /// - public override string ToString() + switch (Type) { - switch (Type) - { - case SmartyPantType.Quote: - case SmartyPantType.LeftQuote: - case SmartyPantType.RightQuote: - return "'"; - case SmartyPantType.DoubleQuote: - return "\""; - case SmartyPantType.LeftDoubleQuote: - return OpeningCharacter == '`' ? "``" : "\""; - case SmartyPantType.RightDoubleQuote: - return OpeningCharacter == '\'' ? "''" : "\""; - case SmartyPantType.Dash2: - return "--"; - case SmartyPantType.Dash3: - return "--"; - case SmartyPantType.LeftAngleQuote: - return "<<"; - case SmartyPantType.RightAngleQuote: - return ">>"; - } - return OpeningCharacter != 0 ? OpeningCharacter.ToString() : string.Empty; + case SmartyPantType.Quote: + case SmartyPantType.LeftQuote: + case SmartyPantType.RightQuote: + return "'"; + case SmartyPantType.DoubleQuote: + return "\""; + case SmartyPantType.LeftDoubleQuote: + return OpeningCharacter == '`' ? "``" : "\""; + case SmartyPantType.RightDoubleQuote: + return OpeningCharacter == '\'' ? "''" : "\""; + case SmartyPantType.Dash2: + return "--"; + case SmartyPantType.Dash3: + return "--"; + case SmartyPantType.LeftAngleQuote: + return "<<"; + case SmartyPantType.RightAngleQuote: + return ">>"; } + return OpeningCharacter != 0 ? OpeningCharacter.ToString() : string.Empty; + } - public LiteralInline AsLiteralInline() + public LiteralInline AsLiteralInline() + { + return new LiteralInline(ToString()) { - return new LiteralInline(ToString()) - { - Span = Span, - Line = Line, - Column = Column, - }; - } + Span = Span, + Line = Line, + Column = Column, + }; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/SmartyPants/SmartyPantOptions.cs b/src/Markdig/Extensions/SmartyPants/SmartyPantOptions.cs index e51a40c2f..7c971c725 100644 --- a/src/Markdig/Extensions/SmartyPants/SmartyPantOptions.cs +++ b/src/Markdig/Extensions/SmartyPants/SmartyPantOptions.cs @@ -2,40 +2,37 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using System.Collections.Generic; +namespace Markdig.Extensions.SmartyPants; -namespace Markdig.Extensions.SmartyPants +/// +/// The options used for . +/// +public class SmartyPantOptions { /// - /// The options used for . + /// Initializes a new instance of the class. /// - public class SmartyPantOptions + public SmartyPantOptions() { - /// - /// Initializes a new instance of the class. - /// - public SmartyPantOptions() + Mapping = new Dictionary() { - Mapping = new Dictionary() - { - {SmartyPantType.Quote, "'"}, - {SmartyPantType.DoubleQuote, "\""}, - {SmartyPantType.LeftQuote, "‘"}, - {SmartyPantType.RightQuote, "’"}, - {SmartyPantType.LeftDoubleQuote, "“"}, - {SmartyPantType.RightDoubleQuote, "”"}, - {SmartyPantType.LeftAngleQuote, "«"}, - {SmartyPantType.RightAngleQuote, "»"}, - {SmartyPantType.Ellipsis, "…"}, - {SmartyPantType.Dash2, "–"}, - {SmartyPantType.Dash3, "—"}, - }; - } - - /// - /// Gets the mapping between a and its textual representation - /// (usually an HTML entity). - /// - public Dictionary Mapping { get; } + {SmartyPantType.Quote, "'"}, + {SmartyPantType.DoubleQuote, "\""}, + {SmartyPantType.LeftQuote, "‘"}, + {SmartyPantType.RightQuote, "’"}, + {SmartyPantType.LeftDoubleQuote, "“"}, + {SmartyPantType.RightDoubleQuote, "”"}, + {SmartyPantType.LeftAngleQuote, "«"}, + {SmartyPantType.RightAngleQuote, "»"}, + {SmartyPantType.Ellipsis, "…"}, + {SmartyPantType.Dash2, "–"}, + {SmartyPantType.Dash3, "—"}, + }; } + + /// + /// Gets the mapping between a and its textual representation + /// (usually an HTML entity). + /// + public Dictionary Mapping { get; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/SmartyPants/SmartyPantType.cs b/src/Markdig/Extensions/SmartyPants/SmartyPantType.cs index b39aa20c5..393dd0e0c 100644 --- a/src/Markdig/Extensions/SmartyPants/SmartyPantType.cs +++ b/src/Markdig/Extensions/SmartyPants/SmartyPantType.cs @@ -2,66 +2,65 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -namespace Markdig.Extensions.SmartyPants +namespace Markdig.Extensions.SmartyPants; + +/// +/// Types of a . +/// +public enum SmartyPantType { /// - /// Types of a . + /// This is a single quote ' /// - public enum SmartyPantType - { - /// - /// This is a single quote ' - /// - Quote = 1, + Quote = 1, - /// - /// This is a left single quote ' -gt; lsquo; - /// - LeftQuote, + /// + /// This is a left single quote ' -gt; lsquo; + /// + LeftQuote, - /// - /// This is a right single quote ' -gt; rsquo; - /// - RightQuote, + /// + /// This is a right single quote ' -gt; rsquo; + /// + RightQuote, - /// - /// This is a double quote " - /// - DoubleQuote, + /// + /// This is a double quote " + /// + DoubleQuote, - /// - /// This is a left double quote " -gt; ldquo; - /// - LeftDoubleQuote, + /// + /// This is a left double quote " -gt; ldquo; + /// + LeftDoubleQuote, - /// - /// This is a right double quote " -gt; rdquo; - /// - RightDoubleQuote, + /// + /// This is a right double quote " -gt; rdquo; + /// + RightDoubleQuote, - /// - /// This is a right double quote << -gt; laquo; - /// - LeftAngleQuote, + /// + /// This is a right double quote << -gt; laquo; + /// + LeftAngleQuote, - /// - /// This is a right angle quote >> -gt; raquo; - /// - RightAngleQuote, + /// + /// This is a right angle quote >> -gt; raquo; + /// + RightAngleQuote, - /// - /// This is an ellipsis ... -gt; hellip; - /// - Ellipsis, + /// + /// This is an ellipsis ... -gt; hellip; + /// + Ellipsis, - /// - /// This is a ndash -- -gt; ndash; - /// - Dash2, + /// + /// This is a ndash -- -gt; ndash; + /// + Dash2, - /// - /// This is a mdash --- -gt; mdash; - /// - Dash3, - } + /// + /// This is a mdash --- -gt; mdash; + /// + Dash3, } \ No newline at end of file diff --git a/src/Markdig/Extensions/SmartyPants/SmartyPantsExtension.cs b/src/Markdig/Extensions/SmartyPants/SmartyPantsExtension.cs index d0d2458fa..1b774d9c2 100644 --- a/src/Markdig/Extensions/SmartyPants/SmartyPantsExtension.cs +++ b/src/Markdig/Extensions/SmartyPants/SmartyPantsExtension.cs @@ -5,44 +5,43 @@ using Markdig.Parsers.Inlines; using Markdig.Renderers; -namespace Markdig.Extensions.SmartyPants +namespace Markdig.Extensions.SmartyPants; + +/// +/// Extension to enable SmartyPants. +/// +public class SmartyPantsExtension : IMarkdownExtension { /// - /// Extension to enable SmartyPants. + /// Initializes a new instance of the class. /// - public class SmartyPantsExtension : IMarkdownExtension + /// The options. + public SmartyPantsExtension(SmartyPantOptions? options) { - /// - /// Initializes a new instance of the class. - /// - /// The options. - public SmartyPantsExtension(SmartyPantOptions? options) - { - Options = options ?? new SmartyPantOptions(); - } + Options = options ?? new SmartyPantOptions(); + } - /// - /// Gets the options. - /// - public SmartyPantOptions Options { get; } + /// + /// Gets the options. + /// + public SmartyPantOptions Options { get; } - public void Setup(MarkdownPipelineBuilder pipeline) + public void Setup(MarkdownPipelineBuilder pipeline) + { + if (!pipeline.InlineParsers.Contains()) { - if (!pipeline.InlineParsers.Contains()) - { - // Insert the parser after the code span parser - pipeline.InlineParsers.InsertAfter(new SmartyPantsInlineParser()); - } + // Insert the parser after the code span parser + pipeline.InlineParsers.InsertAfter(new SmartyPantsInlineParser()); } + } - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + if (renderer is HtmlRenderer htmlRenderer) { - if (renderer is HtmlRenderer htmlRenderer) + if (!htmlRenderer.ObjectRenderers.Contains()) { - if (!htmlRenderer.ObjectRenderers.Contains()) - { - htmlRenderer.ObjectRenderers.Add(new HtmlSmartyPantRenderer(Options)); - } + htmlRenderer.ObjectRenderers.Add(new HtmlSmartyPantRenderer(Options)); } } } diff --git a/src/Markdig/Extensions/SmartyPants/SmartyPantsInlineParser.cs b/src/Markdig/Extensions/SmartyPants/SmartyPantsInlineParser.cs index 3aa732966..641aa804c 100644 --- a/src/Markdig/Extensions/SmartyPants/SmartyPantsInlineParser.cs +++ b/src/Markdig/Extensions/SmartyPants/SmartyPantsInlineParser.cs @@ -2,370 +2,368 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using System.Collections.Generic; using Markdig.Helpers; using Markdig.Parsers; using Markdig.Syntax; using Markdig.Syntax.Inlines; -namespace Markdig.Extensions.SmartyPants +namespace Markdig.Extensions.SmartyPants; + +/// +/// The inline parser for SmartyPants. +/// +public class SmartyPantsInlineParser : InlineParser, IPostInlineProcessor { /// - /// The inline parser for SmartyPants. + /// Initializes a new instance of the class. /// - public class SmartyPantsInlineParser : InlineParser, IPostInlineProcessor + public SmartyPantsInlineParser() { - /// - /// Initializes a new instance of the class. - /// - public SmartyPantsInlineParser() - { - OpeningCharacters = new[] {'\'', '"', '<', '>', '.', '-'}; - } + OpeningCharacters = new[] {'\'', '"', '<', '>', '.', '-'}; + } - public override bool Match(InlineProcessor processor, ref StringSlice slice) - { - // We are matching the following characters: - // - // ' ‘ ’ ‘ ’ 'left-single-quote', 'right-single-quote' - // '' “ ” “ ” 'left-double-quote', 'right-double-quote' - // " “ ” “ ” 'left-double-quote', 'right-double-quote' - // << >> « » « » 'left-angle-quote', 'right-angle-quote' - // ... … … 'ellipsis' + public override bool Match(InlineProcessor processor, ref StringSlice slice) + { + // We are matching the following characters: + // + // ' ‘ ’ ‘ ’ 'left-single-quote', 'right-single-quote' + // '' “ ” “ ” 'left-double-quote', 'right-double-quote' + // " “ ” “ ” 'left-double-quote', 'right-double-quote' + // << >> « » « » 'left-angle-quote', 'right-angle-quote' + // ... … … 'ellipsis' - // Special case: – and — are handle as a PostProcess step to avoid conflicts with pipetables header separator row - // -- – – 'ndash' - // --- — — 'mdash' + // Special case: – and — are handle as a PostProcess step to avoid conflicts with pipetables header separator row + // -- – – 'ndash' + // --- — — 'mdash' - var pc = slice.PeekCharExtra(-1); - var c = slice.CurrentChar; - var openingChar = c; + var pc = slice.PeekCharExtra(-1); + var c = slice.CurrentChar; + var openingChar = c; - var startingPosition = slice.Start; + var startingPosition = slice.Start; - // undefined first - var type = (SmartyPantType) 0; + // undefined first + var type = (SmartyPantType) 0; - switch (c) - { - case '\'': - type = SmartyPantType.Quote; // We will resolve them at the end of parsing all inlines - if (slice.PeekChar() == '\'') - { - slice.SkipChar(); - type = SmartyPantType.DoubleQuote; // We will resolve them at the end of parsing all inlines - } - break; - case '"': - type = SmartyPantType.DoubleQuote; - break; - case '<': - if (slice.NextChar() == '<') - { - type = SmartyPantType.LeftAngleQuote; - } - break; - case '>': - if (slice.NextChar() == '>') - { - type = SmartyPantType.RightAngleQuote; - } - break; - case '.': - if (slice.NextChar() == '.' && slice.NextChar() == '.') - { - type = SmartyPantType.Ellipsis; - } - break; - case '-': - if (slice.NextChar() == '-') - { - var quotePants = GetOrCreateState(processor); - quotePants.HasDash = true; - return false; - } - break; - } + switch (c) + { + case '\'': + type = SmartyPantType.Quote; // We will resolve them at the end of parsing all inlines + if (slice.PeekChar() == '\'') + { + slice.SkipChar(); + type = SmartyPantType.DoubleQuote; // We will resolve them at the end of parsing all inlines + } + break; + case '"': + type = SmartyPantType.DoubleQuote; + break; + case '<': + if (slice.NextChar() == '<') + { + type = SmartyPantType.LeftAngleQuote; + } + break; + case '>': + if (slice.NextChar() == '>') + { + type = SmartyPantType.RightAngleQuote; + } + break; + case '.': + if (slice.NextChar() == '.' && slice.NextChar() == '.') + { + type = SmartyPantType.Ellipsis; + } + break; + case '-': + if (slice.NextChar() == '-') + { + var quotePants = GetOrCreateState(processor); + quotePants.HasDash = true; + return false; + } + break; + } - // If it is not matched, early exit - if (type == 0) - { - return false; - } + // If it is not matched, early exit + if (type == 0) + { + return false; + } - // Skip char - c = slice.NextChar(); + // Skip char + c = slice.NextChar(); - CharHelper.CheckOpenCloseDelimiter(pc, c, false, out bool canOpen, out bool canClose); + CharHelper.CheckOpenCloseDelimiter(pc, c, false, out bool canOpen, out bool canClose); - bool postProcess = false; + bool postProcess = false; - switch (type) - { - case SmartyPantType.Quote: - postProcess = true; - if (canOpen && !canClose) - { - type = SmartyPantType.LeftQuote; - } - else if (!canOpen && canClose) - { - type = SmartyPantType.RightQuote; - } - else - { - return false; - } - break; - case SmartyPantType.DoubleQuote: - postProcess = true; - if (canOpen && !canClose) - { - type = SmartyPantType.LeftDoubleQuote; - } - else if (!canOpen && canClose) - { - type = SmartyPantType.RightDoubleQuote; - } - else - { - return false; - } - break; - case SmartyPantType.LeftAngleQuote: - postProcess = true; - if (!canOpen || canClose) - { - return false; - } - break; - case SmartyPantType.RightAngleQuote: - postProcess = true; - if (canOpen || !canClose) - { - return false; - } - break; - case SmartyPantType.Ellipsis: - if (canOpen || !canClose) - { - return false; - } - break; - } - - // Create the SmartyPant inline - var pant = new SmartyPant() - { - Span = { Start = processor.GetSourcePosition(startingPosition, out int line, out int column) }, - Line = line, - Column = column, - OpeningCharacter = openingChar, - Type = type - }; - pant.Span.End = pant.Span.Start + slice.Start - startingPosition - 1; - - // We will check in a post-process step for balanced open/close quotes - if (postProcess) - { - var quotePants = GetOrCreateState(processor); - - // Register only if we don't have yet any quotes - if (quotePants.Count is 0) + switch (type) + { + case SmartyPantType.Quote: + postProcess = true; + if (canOpen && !canClose) { - processor.Block!.ProcessInlinesEnd += BlockOnProcessInlinesEnd; + type = SmartyPantType.LeftQuote; } - quotePants.Add(pant); - } - - processor.Inline = pant; - return true; + else if (!canOpen && canClose) + { + type = SmartyPantType.RightQuote; + } + else + { + return false; + } + break; + case SmartyPantType.DoubleQuote: + postProcess = true; + if (canOpen && !canClose) + { + type = SmartyPantType.LeftDoubleQuote; + } + else if (!canOpen && canClose) + { + type = SmartyPantType.RightDoubleQuote; + } + else + { + return false; + } + break; + case SmartyPantType.LeftAngleQuote: + postProcess = true; + if (!canOpen || canClose) + { + return false; + } + break; + case SmartyPantType.RightAngleQuote: + postProcess = true; + if (canOpen || !canClose) + { + return false; + } + break; + case SmartyPantType.Ellipsis: + if (canOpen || !canClose) + { + return false; + } + break; } - private ListSmartyPants GetOrCreateState(InlineProcessor processor) + // Create the SmartyPant inline + var pant = new SmartyPant() + { + Span = { Start = processor.GetSourcePosition(startingPosition, out int line, out int column) }, + Line = line, + Column = column, + OpeningCharacter = openingChar, + Type = type + }; + pant.Span.End = pant.Span.Start + slice.Start - startingPosition - 1; + + // We will check in a post-process step for balanced open/close quotes + if (postProcess) { - if (!(processor.ParserStates[Index] is ListSmartyPants quotePants)) + var quotePants = GetOrCreateState(processor); + + // Register only if we don't have yet any quotes + if (quotePants.Count is 0) { - processor.ParserStates[Index] = quotePants = new ListSmartyPants(); + processor.Block!.ProcessInlinesEnd += BlockOnProcessInlinesEnd; } - return quotePants; + quotePants.Add(pant); } - private readonly struct Opener - { - public readonly int Type; - public readonly int Index; + processor.Inline = pant; + return true; + } - public Opener(int type, int index) - { - Type = type; - Index = index; - } + private ListSmartyPants GetOrCreateState(InlineProcessor processor) + { + if (!(processor.ParserStates[Index] is ListSmartyPants quotePants)) + { + processor.ParserStates[Index] = quotePants = new ListSmartyPants(); } + return quotePants; + } - private void BlockOnProcessInlinesEnd(InlineProcessor processor, Inline? inline) + private readonly struct Opener + { + public readonly int Type; + public readonly int Index; + + public Opener(int type, int index) { - processor.Block!.ProcessInlinesEnd -= BlockOnProcessInlinesEnd; + Type = type; + Index = index; + } + } - var pants = (ListSmartyPants) processor.ParserStates[Index]; + private void BlockOnProcessInlinesEnd(InlineProcessor processor, Inline? inline) + { + processor.Block!.ProcessInlinesEnd -= BlockOnProcessInlinesEnd; - var openers = new Stack(4); + var pants = (ListSmartyPants) processor.ParserStates[Index]; - for (int i = 0; i < pants.Count; i++) - { - var quote = pants[i]; - var quoteType = quote.Type; + var openers = new Stack(4); - int type; - bool isLeft; + for (int i = 0; i < pants.Count; i++) + { + var quote = pants[i]; + var quoteType = quote.Type; - if (quoteType is SmartyPantType.LeftQuote or SmartyPantType.RightQuote) - { - type = 0; - isLeft = quoteType == SmartyPantType.LeftQuote; - } - else if (quoteType is SmartyPantType.LeftDoubleQuote or SmartyPantType.RightDoubleQuote) - { - type = 1; - isLeft = quoteType == SmartyPantType.LeftDoubleQuote; - } - else if (quoteType is SmartyPantType.LeftAngleQuote or SmartyPantType.RightAngleQuote) - { - type = 2; - isLeft = quoteType == SmartyPantType.LeftAngleQuote; - } - else - { - quote.ReplaceBy(quote.AsLiteralInline()); - continue; - } + int type; + bool isLeft; - if (isLeft) - { - openers.Push(new Opener(type, i)); - } - else + if (quoteType is SmartyPantType.LeftQuote or SmartyPantType.RightQuote) + { + type = 0; + isLeft = quoteType == SmartyPantType.LeftQuote; + } + else if (quoteType is SmartyPantType.LeftDoubleQuote or SmartyPantType.RightDoubleQuote) + { + type = 1; + isLeft = quoteType == SmartyPantType.LeftDoubleQuote; + } + else if (quoteType is SmartyPantType.LeftAngleQuote or SmartyPantType.RightAngleQuote) + { + type = 2; + isLeft = quoteType == SmartyPantType.LeftAngleQuote; + } + else + { + quote.ReplaceBy(quote.AsLiteralInline()); + continue; + } + + if (isLeft) + { + openers.Push(new Opener(type, i)); + } + else + { + bool found = false; + + while (openers.Count > 0) { - bool found = false; + Opener opener = openers.Pop(); + var previousQuote = pants[opener.Index]; - while (openers.Count > 0) + if (opener.Type == type) { - Opener opener = openers.Pop(); - var previousQuote = pants[opener.Index]; - - if (opener.Type == type) - { - found = true; - break; - } - else - { - previousQuote.ReplaceBy(previousQuote.AsLiteralInline()); - } + found = true; + break; } - - if (!found) + else { - quote.ReplaceBy(quote.AsLiteralInline()); + previousQuote.ReplaceBy(previousQuote.AsLiteralInline()); } } - } - foreach (var opener in openers) - { - var quote = pants[opener.Index]; - quote.ReplaceBy(quote.AsLiteralInline()); + if (!found) + { + quote.ReplaceBy(quote.AsLiteralInline()); + } } + } - pants.Clear(); + foreach (var opener in openers) + { + var quote = pants[opener.Index]; + quote.ReplaceBy(quote.AsLiteralInline()); } - bool IPostInlineProcessor.PostProcess( - InlineProcessor state, - Inline? root, - Inline? lastChild, - int postInlineProcessorIndex, - bool isFinalProcessing) + pants.Clear(); + } + + bool IPostInlineProcessor.PostProcess( + InlineProcessor state, + Inline? root, + Inline? lastChild, + int postInlineProcessorIndex, + bool isFinalProcessing) + { + // Don't try to process anything if there are no dash + if (!(state.ParserStates[Index] is ListSmartyPants quotePants) || !quotePants.HasDash) { - // Don't try to process anything if there are no dash - if (!(state.ParserStates[Index] is ListSmartyPants quotePants) || !quotePants.HasDash) - { - return true; - } + return true; + } - var child = root; - var pendingContainers = new Stack(); + var child = root; + var pendingContainers = new Stack(); - while (true) + while (true) + { + while (child != null) { - while (child != null) + var next = child.NextSibling; + + if (child is LiteralInline literal) { - var next = child.NextSibling; + var startIndex = 0; - if (child is LiteralInline literal) + var indexOfDash = literal.Content.IndexOf("--", startIndex); + if (indexOfDash >= 0) { - var startIndex = 0; - - var indexOfDash = literal.Content.IndexOf("--", startIndex); - if (indexOfDash >= 0) + var type = SmartyPantType.Dash2; + if (literal.Content.PeekCharAbsolute(indexOfDash + 2) == '-') { - var type = SmartyPantType.Dash2; - if (literal.Content.PeekCharAbsolute(indexOfDash + 2) == '-') - { - type = SmartyPantType.Dash3; - } - var nextContent = literal.Content; - var originalSpan = literal.Span; - literal.Span.End -= literal.Content.End - indexOfDash + 1; - literal.Content.End = indexOfDash - 1; - nextContent.Start = indexOfDash + (type == SmartyPantType.Dash2 ? 2 : 3); - - var pant = new SmartyPant() - { - Span = new SourceSpan(literal.Content.End + 1, nextContent.Start - 1), - Line = literal.Line, - Column = literal.Column, - OpeningCharacter = '-', - Type = type - }; - literal.InsertAfter(pant); - - var postLiteral = new LiteralInline() - { - Span = new SourceSpan(pant.Span.End + 1, originalSpan.End), - Line = literal.Line, - Column = literal.Column, - Content = nextContent - }; - pant.InsertAfter(postLiteral); - - // Use the pending literal to proceed further - next = postLiteral; + type = SmartyPantType.Dash3; } - } - else if (child is ContainerInline childContainer) - { - pendingContainers.Push(childContainer.FirstChild!); - } + var nextContent = literal.Content; + var originalSpan = literal.Span; + literal.Span.End -= literal.Content.End - indexOfDash + 1; + literal.Content.End = indexOfDash - 1; + nextContent.Start = indexOfDash + (type == SmartyPantType.Dash2 ? 2 : 3); - child = next; - } - if (pendingContainers.Count > 0) - { - child = pendingContainers.Pop(); + var pant = new SmartyPant() + { + Span = new SourceSpan(literal.Content.End + 1, nextContent.Start - 1), + Line = literal.Line, + Column = literal.Column, + OpeningCharacter = '-', + Type = type + }; + literal.InsertAfter(pant); + + var postLiteral = new LiteralInline() + { + Span = new SourceSpan(pant.Span.End + 1, originalSpan.End), + Line = literal.Line, + Column = literal.Column, + Content = nextContent + }; + pant.InsertAfter(postLiteral); + + // Use the pending literal to proceed further + next = postLiteral; + } } - else + else if (child is ContainerInline childContainer) { - break; + pendingContainers.Push(childContainer.FirstChild!); } + + child = next; + } + if (pendingContainers.Count > 0) + { + child = pendingContainers.Pop(); + } + else + { + break; } - return true; } + return true; + } - private sealed class ListSmartyPants : List - { - public bool HasDash { get; set; } - } + private sealed class ListSmartyPants : List + { + public bool HasDash { get; set; } } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Tables/GridTableExtension.cs b/src/Markdig/Extensions/Tables/GridTableExtension.cs index f890dfa30..12ac5d15e 100644 --- a/src/Markdig/Extensions/Tables/GridTableExtension.cs +++ b/src/Markdig/Extensions/Tables/GridTableExtension.cs @@ -4,28 +4,27 @@ using Markdig.Renderers; -namespace Markdig.Extensions.Tables +namespace Markdig.Extensions.Tables; + +/// +/// Extension that allows to use grid tables. +/// +/// +public class GridTableExtension : IMarkdownExtension { - /// - /// Extension that allows to use grid tables. - /// - /// - public class GridTableExtension : IMarkdownExtension + public void Setup(MarkdownPipelineBuilder pipeline) { - public void Setup(MarkdownPipelineBuilder pipeline) + if (!pipeline.BlockParsers.Contains()) { - if (!pipeline.BlockParsers.Contains()) - { - pipeline.BlockParsers.Insert(0, new GridTableParser()); - } + pipeline.BlockParsers.Insert(0, new GridTableParser()); } + } - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + if (renderer is HtmlRenderer htmlRenderer && !htmlRenderer.ObjectRenderers.Contains()) { - if (renderer is HtmlRenderer htmlRenderer && !htmlRenderer.ObjectRenderers.Contains()) - { - htmlRenderer.ObjectRenderers.Add(new HtmlTableRenderer()); - } + htmlRenderer.ObjectRenderers.Add(new HtmlTableRenderer()); } } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Tables/GridTableParser.cs b/src/Markdig/Extensions/Tables/GridTableParser.cs index 49be8cae1..ae6048eb7 100644 --- a/src/Markdig/Extensions/Tables/GridTableParser.cs +++ b/src/Markdig/Extensions/Tables/GridTableParser.cs @@ -2,362 +2,360 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using System.Collections.Generic; using Markdig.Helpers; using Markdig.Parsers; using Markdig.Syntax; -namespace Markdig.Extensions.Tables +namespace Markdig.Extensions.Tables; + +public class GridTableParser : BlockParser { - public class GridTableParser : BlockParser + public GridTableParser() { - public GridTableParser() + OpeningCharacters = new[] { '+' }; + } + + public override BlockState TryOpen(BlockProcessor processor) + { + // A grid table cannot start more than an indent + if (processor.IsCodeIndent) { - OpeningCharacters = new[] { '+' }; + return BlockState.None; } - public override BlockState TryOpen(BlockProcessor processor) - { - // A grid table cannot start more than an indent - if (processor.IsCodeIndent) - { - return BlockState.None; - } + var line = processor.Line; + GridTableState? tableState = null; - var line = processor.Line; - GridTableState? tableState = null; + // Match the first row that should be of the minimal form: +--------------- + var c = line.CurrentChar; + var lineStart = line.Start; + while (c == '+') + { + var columnStart = line.Start; + line.SkipChar(); + line.TrimStart(); - // Match the first row that should be of the minimal form: +--------------- - var c = line.CurrentChar; - var lineStart = line.Start; - while (c == '+') + // if we have reached the end of the line, exit + c = line.CurrentChar; + if (c == 0) { - var columnStart = line.Start; - line.SkipChar(); - line.TrimStart(); - - // if we have reached the end of the line, exit - c = line.CurrentChar; - if (c == 0) - { - break; - } - - // Parse a column alignment - if (!TableHelper.ParseColumnHeader(ref line, '-', out TableColumnAlign? columnAlign)) - { - return BlockState.None; - } - - tableState ??= new GridTableState(start: processor.Start, expectRow: true); - tableState.AddColumn(columnStart - lineStart, line.Start - lineStart, columnAlign); - - c = line.CurrentChar; + break; } - if (c != 0 || tableState is null) + // Parse a column alignment + if (!TableHelper.ParseColumnHeader(ref line, '-', out TableColumnAlign? columnAlign)) { return BlockState.None; } - // Store the line (if we need later to build a ParagraphBlock because the GridTable was in fact invalid) - tableState.AddLine(ref processor.Line); - var table = new Table(this); - table.SetData(typeof(GridTableState), tableState); - - // Calculate the total width of all columns - int totalWidth = 0; - foreach (var columnSlice in tableState.ColumnSlices!) - { - totalWidth += columnSlice.End - columnSlice.Start - 1; - } - // Store the column width and alignment - foreach (var columnSlice in tableState.ColumnSlices) - { - var columnDefinition = new TableColumnDefinition - { - // Column width proportional to the total width - Width = (float)(columnSlice.End - columnSlice.Start - 1) * 100.0f / totalWidth, - Alignment = columnSlice.Align - }; - table.ColumnDefinitions.Add(columnDefinition); - } + tableState ??= new GridTableState(start: processor.Start, expectRow: true); + tableState.AddColumn(columnStart - lineStart, line.Start - lineStart, columnAlign); - processor.NewBlocks.Push(table); + c = line.CurrentChar; + } - return BlockState.ContinueDiscard; + if (c != 0 || tableState is null) + { + return BlockState.None; + } + // Store the line (if we need later to build a ParagraphBlock because the GridTable was in fact invalid) + tableState.AddLine(ref processor.Line); + var table = new Table(this); + table.SetData(typeof(GridTableState), tableState); + + // Calculate the total width of all columns + int totalWidth = 0; + foreach (var columnSlice in tableState.ColumnSlices!) + { + totalWidth += columnSlice.End - columnSlice.Start - 1; } - public override BlockState TryContinue(BlockProcessor processor, Block block) + // Store the column width and alignment + foreach (var columnSlice in tableState.ColumnSlices) { - var gridTable = (Table)block; - var tableState = (GridTableState)block.GetData(typeof(GridTableState))!; - tableState.AddLine(ref processor.Line); - if (processor.CurrentChar == '+') - { - return HandleNewRow(processor, tableState, gridTable); - } - if (processor.CurrentChar == '|') - { - return HandleContents(processor, tableState, gridTable); - } - TerminateCurrentRow(processor, tableState, gridTable, true); - // If the table is not valid we need to remove the grid table, - // and create a ParagraphBlock with the slices - if (!gridTable.IsValid()) + var columnDefinition = new TableColumnDefinition { - Undo(processor, tableState, gridTable); - } - return BlockState.Break; + // Column width proportional to the total width + Width = (float)(columnSlice.End - columnSlice.Start - 1) * 100.0f / totalWidth, + Alignment = columnSlice.Align + }; + table.ColumnDefinitions.Add(columnDefinition); } - private BlockState HandleNewRow(BlockProcessor processor, GridTableState tableState, Table gridTable) + processor.NewBlocks.Push(table); + + return BlockState.ContinueDiscard; + } + + public override BlockState TryContinue(BlockProcessor processor, Block block) + { + var gridTable = (Table)block; + var tableState = (GridTableState)block.GetData(typeof(GridTableState))!; + tableState.AddLine(ref processor.Line); + if (processor.CurrentChar == '+') { - var columns = tableState.ColumnSlices!; - SetRowSpanState(columns, processor.Line, out bool isHeaderRow, out bool hasRowSpan); - SetColumnSpanState(columns, processor.Line); - TerminateCurrentRow(processor, tableState, gridTable, false); - if (isHeaderRow) - { - for (int i = 0; i < gridTable.Count; i++) - { - var row = (TableRow)gridTable[i]; - row.IsHeader = true; - } - } - tableState.StartRowGroup = gridTable.Count; - if (hasRowSpan) + return HandleNewRow(processor, tableState, gridTable); + } + if (processor.CurrentChar == '|') + { + return HandleContents(processor, tableState, gridTable); + } + TerminateCurrentRow(processor, tableState, gridTable, true); + // If the table is not valid we need to remove the grid table, + // and create a ParagraphBlock with the slices + if (!gridTable.IsValid()) + { + Undo(processor, tableState, gridTable); + } + return BlockState.Break; + } + + private BlockState HandleNewRow(BlockProcessor processor, GridTableState tableState, Table gridTable) + { + var columns = tableState.ColumnSlices!; + SetRowSpanState(columns, processor.Line, out bool isHeaderRow, out bool hasRowSpan); + SetColumnSpanState(columns, processor.Line); + TerminateCurrentRow(processor, tableState, gridTable, false); + if (isHeaderRow) + { + for (int i = 0; i < gridTable.Count; i++) { - HandleContents(processor, tableState, gridTable); + var row = (TableRow)gridTable[i]; + row.IsHeader = true; } - return BlockState.ContinueDiscard; } + tableState.StartRowGroup = gridTable.Count; + if (hasRowSpan) + { + HandleContents(processor, tableState, gridTable); + } + return BlockState.ContinueDiscard; + } - private static void SetRowSpanState(List columns, StringSlice line, out bool isHeaderRow, out bool hasRowSpan) + private static void SetRowSpanState(List columns, StringSlice line, out bool isHeaderRow, out bool hasRowSpan) + { + var lineStart = line.Start; + isHeaderRow = line.PeekChar() == '=' || line.PeekChar(2) == '='; + hasRowSpan = false; + foreach (var columnSlice in columns) { - var lineStart = line.Start; - isHeaderRow = line.PeekChar() == '=' || line.PeekChar(2) == '='; - hasRowSpan = false; - foreach (var columnSlice in columns) + if (columnSlice.CurrentCell != null) { - if (columnSlice.CurrentCell != null) + line.Start = lineStart + columnSlice.Start + 1; + line.End = lineStart + columnSlice.End - 1; + line.Trim(); + if (line.IsEmptyOrWhitespace() || !IsRowSeperator(line)) { - line.Start = lineStart + columnSlice.Start + 1; - line.End = lineStart + columnSlice.End - 1; - line.Trim(); - if (line.IsEmptyOrWhitespace() || !IsRowSeperator(line)) - { - hasRowSpan = true; - columnSlice.CurrentCell.RowSpan++; - columnSlice.CurrentCell.AllowClose = false; - } - else - { - columnSlice.CurrentCell.AllowClose = true; - } + hasRowSpan = true; + columnSlice.CurrentCell.RowSpan++; + columnSlice.CurrentCell.AllowClose = false; + } + else + { + columnSlice.CurrentCell.AllowClose = true; } } } + } - private static bool IsRowSeperator(StringSlice slice) + private static bool IsRowSeperator(StringSlice slice) + { + char c = slice.CurrentChar; + do { - char c = slice.CurrentChar; - do + if (c != '-' && c != '=' && c != ':') { - if (c != '-' && c != '=' && c != ':') - { - return c == '\0'; - } - c = slice.NextChar(); + return c == '\0'; } - while (true); + c = slice.NextChar(); } + while (true); + } - private static void TerminateCurrentRow(BlockProcessor processor, GridTableState tableState, Table gridTable, bool isLastRow) + private static void TerminateCurrentRow(BlockProcessor processor, GridTableState tableState, Table gridTable, bool isLastRow) + { + var columns = tableState.ColumnSlices; + TableRow? currentRow = null; + for (int i = 0; i < columns!.Count; i++) { - var columns = tableState.ColumnSlices; - TableRow? currentRow = null; - for (int i = 0; i < columns!.Count; i++) + var columnSlice = columns[i]; + if (columnSlice.CurrentCell != null) { - var columnSlice = columns[i]; - if (columnSlice.CurrentCell != null) + currentRow ??= new TableRow(); + + // If this cell does not already belong to a row + if (columnSlice.CurrentCell.Parent is null) { - currentRow ??= new TableRow(); - - // If this cell does not already belong to a row - if (columnSlice.CurrentCell.Parent is null) - { - currentRow.Add(columnSlice.CurrentCell); - } - // If the cell is not going to span through to the next row - if (columnSlice.CurrentCell.AllowClose) - { - columnSlice.BlockProcessor!.Close(columnSlice.CurrentCell); - } + currentRow.Add(columnSlice.CurrentCell); } - - // Renew the block parser processor (or reset it for the last row) - if (columnSlice.BlockProcessor != null && (columnSlice.CurrentCell is null || columnSlice.CurrentCell.AllowClose)) + // If the cell is not going to span through to the next row + if (columnSlice.CurrentCell.AllowClose) { - columnSlice.BlockProcessor.ReleaseChild(); - columnSlice.BlockProcessor = isLastRow ? null : processor.CreateChild(); + columnSlice.BlockProcessor!.Close(columnSlice.CurrentCell); } + } - // Create or erase the cell - if (isLastRow || columnSlice.CurrentColumnSpan == 0 || (columnSlice.CurrentCell != null && columnSlice.CurrentCell.AllowClose)) - { - // We don't need the cell anymore if we have a last row - // Or the cell has a columnspan == 0 - // And the cell does not have to be kept open to span rows - columnSlice.CurrentCell = null; - } + // Renew the block parser processor (or reset it for the last row) + if (columnSlice.BlockProcessor != null && (columnSlice.CurrentCell is null || columnSlice.CurrentCell.AllowClose)) + { + columnSlice.BlockProcessor.ReleaseChild(); + columnSlice.BlockProcessor = isLastRow ? null : processor.CreateChild(); } - if (currentRow is { Count: > 0 }) + // Create or erase the cell + if (isLastRow || columnSlice.CurrentColumnSpan == 0 || (columnSlice.CurrentCell != null && columnSlice.CurrentCell.AllowClose)) { - gridTable.Add(currentRow); + // We don't need the cell anymore if we have a last row + // Or the cell has a columnspan == 0 + // And the cell does not have to be kept open to span rows + columnSlice.CurrentCell = null; } } - private BlockState HandleContents(BlockProcessor processor, GridTableState tableState, Table gridTable) + if (currentRow is { Count: > 0 }) + { + gridTable.Add(currentRow); + } + } + + private BlockState HandleContents(BlockProcessor processor, GridTableState tableState, Table gridTable) + { + var isRowLine = processor.CurrentChar == '+'; + var columns = tableState.ColumnSlices!; + var line = processor.Line; + SetColumnSpanState(columns, line); + if (!isRowLine && !CanContinueRow(columns)) { - var isRowLine = processor.CurrentChar == '+'; - var columns = tableState.ColumnSlices!; - var line = processor.Line; - SetColumnSpanState(columns, line); - if (!isRowLine && !CanContinueRow(columns)) + TerminateCurrentRow(processor, tableState, gridTable, false); + } + for (int i = 0; i < columns.Count;) + { + var columnSlice = columns[i]; + var nextColumnIndex = i + columnSlice.CurrentColumnSpan; + // If the span is 0, we exit + if (nextColumnIndex == i) + { + break; + } + var nextColumn = nextColumnIndex < columns.Count ? columns[nextColumnIndex] : null; + + var sliceForCell = line; + sliceForCell.Start = line.Start + columnSlice.Start + 1; + if (nextColumn != null) { - TerminateCurrentRow(processor, tableState, gridTable, false); + sliceForCell.End = line.Start + nextColumn.Start - 1; } - for (int i = 0; i < columns.Count;) + else { - var columnSlice = columns[i]; - var nextColumnIndex = i + columnSlice.CurrentColumnSpan; - // If the span is 0, we exit - if (nextColumnIndex == i) + var columnEnd = columns[columns.Count - 1].End; + var columnEndChar = line.PeekCharExtra(columnEnd); + // If there is a `|` (or a `+` in the case that we are dealing with a row line + // with spanned contents) exactly at the expected end of the table row, we cut the line + // otherwise we allow to have the last cell of a row to be open for longer cell content + if (columnEndChar == '|' || (isRowLine && columnEndChar == '+')) { - break; + sliceForCell.End = line.Start + columnEnd - 1; } - var nextColumn = nextColumnIndex < columns.Count ? columns[nextColumnIndex] : null; - - var sliceForCell = line; - sliceForCell.Start = line.Start + columnSlice.Start + 1; - if (nextColumn != null) + else if (line.PeekCharExtra(line.End) == '|') { - sliceForCell.End = line.Start + nextColumn.Start - 1; + sliceForCell.End = line.End - 1; } - else + } + sliceForCell.TrimEnd(); + + if (!isRowLine || !IsRowSeperator(sliceForCell)) + { + if (columnSlice.CurrentCell is null) { - var columnEnd = columns[columns.Count - 1].End; - var columnEndChar = line.PeekCharExtra(columnEnd); - // If there is a `|` (or a `+` in the case that we are dealing with a row line - // with spanned contents) exactly at the expected end of the table row, we cut the line - // otherwise we allow to have the last cell of a row to be open for longer cell content - if (columnEndChar == '|' || (isRowLine && columnEndChar == '+')) - { - sliceForCell.End = line.Start + columnEnd - 1; - } - else if (line.PeekCharExtra(line.End) == '|') + columnSlice.CurrentCell = new TableCell(this) { - sliceForCell.End = line.End - 1; - } - } - sliceForCell.TrimEnd(); + ColumnSpan = columnSlice.CurrentColumnSpan, + ColumnIndex = i + }; - if (!isRowLine || !IsRowSeperator(sliceForCell)) - { - if (columnSlice.CurrentCell is null) + if (columnSlice.BlockProcessor is null) { - columnSlice.CurrentCell = new TableCell(this) - { - ColumnSpan = columnSlice.CurrentColumnSpan, - ColumnIndex = i - }; - - if (columnSlice.BlockProcessor is null) - { - columnSlice.BlockProcessor = processor.CreateChild(); - } - - // Ensure that the BlockParser is aware that the TableCell is the top-level container - columnSlice.BlockProcessor.Open(columnSlice.CurrentCell); + columnSlice.BlockProcessor = processor.CreateChild(); } - // Process the content of the cell - columnSlice.BlockProcessor!.LineIndex = processor.LineIndex; - columnSlice.BlockProcessor.ProcessLine(sliceForCell); - } - // Go to next column - i = nextColumnIndex; + // Ensure that the BlockParser is aware that the TableCell is the top-level container + columnSlice.BlockProcessor.Open(columnSlice.CurrentCell); + } + // Process the content of the cell + columnSlice.BlockProcessor!.LineIndex = processor.LineIndex; + columnSlice.BlockProcessor.ProcessLine(sliceForCell); } - return BlockState.ContinueDiscard; + + // Go to next column + i = nextColumnIndex; } + return BlockState.ContinueDiscard; + } - private static void SetColumnSpanState(List columns, StringSlice line) + private static void SetColumnSpanState(List columns, StringSlice line) + { + foreach (var columnSlice in columns) + { + columnSlice.PreviousColumnSpan = columnSlice.CurrentColumnSpan; + columnSlice.CurrentColumnSpan = 0; + } + // | ------------- | ------------ | ---------------------------------------- | + // Calculate the colspan for the new row + int columnIndex = -1; + for (int i = 0; i < columns.Count; i++) { - foreach (var columnSlice in columns) + var columnSlice = columns[i]; + var peek = line.PeekChar(columnSlice.Start); + if (peek == '|' || peek == '+') { - columnSlice.PreviousColumnSpan = columnSlice.CurrentColumnSpan; - columnSlice.CurrentColumnSpan = 0; + columnIndex = i; } - // | ------------- | ------------ | ---------------------------------------- | - // Calculate the colspan for the new row - int columnIndex = -1; - for (int i = 0; i < columns.Count; i++) + if (columnIndex >= 0) { - var columnSlice = columns[i]; - var peek = line.PeekChar(columnSlice.Start); - if (peek == '|' || peek == '+') - { - columnIndex = i; - } - if (columnIndex >= 0) - { - columns[columnIndex].CurrentColumnSpan++; - } + columns[columnIndex].CurrentColumnSpan++; } } + } - private static bool CanContinueRow(List columns) + private static bool CanContinueRow(List columns) + { + foreach (var columnSlice in columns) { - foreach (var columnSlice in columns) + if (columnSlice.PreviousColumnSpan != columnSlice.CurrentColumnSpan) { - if (columnSlice.PreviousColumnSpan != columnSlice.CurrentColumnSpan) - { - return false; - } + return false; } - return true; } + return true; + } - private static void Undo(BlockProcessor processor, GridTableState tableState, Table gridTable) + private static void Undo(BlockProcessor processor, GridTableState tableState, Table gridTable) + { + var parser = processor.Parsers.FindExact(); + // Discard the grid table + var parent = gridTable.Parent!; + processor.Discard(gridTable); + var paragraphBlock = new ParagraphBlock(parser) { - var parser = processor.Parsers.FindExact(); - // Discard the grid table - var parent = gridTable.Parent!; - processor.Discard(gridTable); - var paragraphBlock = new ParagraphBlock(parser) - { - Lines = tableState.Lines, - }; - parent.Add(paragraphBlock); - processor.Open(paragraphBlock); - } + Lines = tableState.Lines, + }; + parent.Add(paragraphBlock); + processor.Open(paragraphBlock); + } - public override bool Close(BlockProcessor processor, Block block) + public override bool Close(BlockProcessor processor, Block block) + { + // Work only on Table, not on TableCell + var gridTable = block as Table; + if (gridTable != null) { - // Work only on Table, not on TableCell - var gridTable = block as Table; - if (gridTable != null) + var tableState = (GridTableState)block.GetData(typeof(GridTableState))!; + TerminateCurrentRow(processor, tableState, gridTable, true); + if (!gridTable.IsValid()) { - var tableState = (GridTableState)block.GetData(typeof(GridTableState))!; - TerminateCurrentRow(processor, tableState, gridTable, true); - if (!gridTable.IsValid()) - { - Undo(processor, tableState, gridTable); - } + Undo(processor, tableState, gridTable); } - return true; } + return true; } } diff --git a/src/Markdig/Extensions/Tables/GridTableState.cs b/src/Markdig/Extensions/Tables/GridTableState.cs index 87bf4a75e..bb1f5f1ee 100644 --- a/src/Markdig/Extensions/Tables/GridTableState.cs +++ b/src/Markdig/Extensions/Tables/GridTableState.cs @@ -2,76 +2,74 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using System.Collections.Generic; using Markdig.Helpers; using Markdig.Parsers; -namespace Markdig.Extensions.Tables +namespace Markdig.Extensions.Tables; + +/// +/// Internal state used by the +/// +internal sealed class GridTableState { - /// - /// Internal state used by the - /// - internal sealed class GridTableState + public GridTableState(int start, bool expectRow) { - public GridTableState(int start, bool expectRow) - { - Start = start; - ExpectRow = expectRow; - } + Start = start; + ExpectRow = expectRow; + } - public int Start { get; } + public int Start { get; } - public StringLineGroup Lines; + public StringLineGroup Lines; - public List? ColumnSlices { get; private set; } + public List? ColumnSlices { get; private set; } - public bool ExpectRow { get; } + public bool ExpectRow { get; } - public int StartRowGroup { get; set; } + public int StartRowGroup { get; set; } - public void AddLine(ref StringSlice line) + public void AddLine(ref StringSlice line) + { + if (Lines.Lines is null) { - if (Lines.Lines is null) - { - Lines = new StringLineGroup(4); - } - - Lines.Add(line); + Lines = new StringLineGroup(4); } - public void AddColumn(int start, int end, TableColumnAlign? align) - { - ColumnSlices ??= new List(); - - ColumnSlices.Add(new ColumnSlice(start, end, align)); - } + Lines.Add(line); + } - public sealed class ColumnSlice + public void AddColumn(int start, int end, TableColumnAlign? align) + { + ColumnSlices ??= new List(); + + ColumnSlices.Add(new ColumnSlice(start, end, align)); + } + + public sealed class ColumnSlice + { + public ColumnSlice(int start, int end, TableColumnAlign? align) { - public ColumnSlice(int start, int end, TableColumnAlign? align) - { - Start = start; - End = end; - Align = align; - CurrentColumnSpan = -1; - } + Start = start; + End = end; + Align = align; + CurrentColumnSpan = -1; + } - /// - /// Gets or sets the index position of this column (after the |) - /// - public int Start { get; } + /// + /// Gets or sets the index position of this column (after the |) + /// + public int Start { get; } - public int End { get; } + public int End { get; } - public TableColumnAlign? Align { get; } + public TableColumnAlign? Align { get; } - public int CurrentColumnSpan { get; set; } + public int CurrentColumnSpan { get; set; } - public int PreviousColumnSpan { get; set; } + public int PreviousColumnSpan { get; set; } - public BlockProcessor? BlockProcessor { get; set; } + public BlockProcessor? BlockProcessor { get; set; } - public TableCell? CurrentCell { get; set; } - } + public TableCell? CurrentCell { get; set; } } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Tables/HtmlTableRenderer.cs b/src/Markdig/Extensions/Tables/HtmlTableRenderer.cs index 443f18f0f..4c7e270e7 100644 --- a/src/Markdig/Extensions/Tables/HtmlTableRenderer.cs +++ b/src/Markdig/Extensions/Tables/HtmlTableRenderer.cs @@ -2,169 +2,168 @@ // 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.Globalization; + using Markdig.Renderers; using Markdig.Renderers.Html; -namespace Markdig.Extensions.Tables +namespace Markdig.Extensions.Tables; + +/// +/// A HTML renderer for a +/// +/// +public class HtmlTableRenderer : HtmlObjectRenderer { - /// - /// A HTML renderer for a - /// - /// - public class HtmlTableRenderer : HtmlObjectRenderer
    + protected override void Write(HtmlRenderer renderer, Table table) { - protected override void Write(HtmlRenderer renderer, Table table) + if (renderer.EnableHtmlForBlock) { - if (renderer.EnableHtmlForBlock) - { - renderer.EnsureLine(); - renderer.Write("'); + renderer.EnsureLine(); + renderer.Write("'); - bool hasBody = false; - bool hasAlreadyHeader = false; - bool isHeaderOpen = false; + bool hasBody = false; + bool hasAlreadyHeader = false; + bool isHeaderOpen = false; - bool hasColumnWidth = false; + bool hasColumnWidth = false; + foreach (var tableColumnDefinition in table.ColumnDefinitions) + { + if (tableColumnDefinition.Width != 0.0f && tableColumnDefinition.Width != 1.0f) + { + hasColumnWidth = true; + break; + } + } + + if (hasColumnWidth) + { foreach (var tableColumnDefinition in table.ColumnDefinitions) { - if (tableColumnDefinition.Width != 0.0f && tableColumnDefinition.Width != 1.0f) + var width = Math.Round(tableColumnDefinition.Width * 100) / 100; + var widthValue = string.Format(CultureInfo.InvariantCulture, "{0:0.##}", width); + renderer.WriteLine($""); + } + } + + foreach (var rowObj in table) + { + var row = (TableRow)rowObj; + if (row.IsHeader) + { + // Allow a single thead + if (!hasAlreadyHeader) { - hasColumnWidth = true; - break; + renderer.WriteLine(""); + isHeaderOpen = true; } - } - if (hasColumnWidth) + hasAlreadyHeader = true; + } + else if (!hasBody) { - foreach (var tableColumnDefinition in table.ColumnDefinitions) + if (isHeaderOpen) { - var width = Math.Round(tableColumnDefinition.Width * 100) / 100; - var widthValue = string.Format(CultureInfo.InvariantCulture, "{0:0.##}", width); - renderer.WriteLine($""); + renderer.WriteLine(""); + isHeaderOpen = false; } + + renderer.WriteLine(""); + hasBody = true; } - foreach (var rowObj in table) + renderer.Write("'); + for (int i = 0; i < row.Count; i++) { - var row = (TableRow)rowObj; - if (row.IsHeader) - { - // Allow a single thead - if (!hasAlreadyHeader) - { - renderer.WriteLine(""); - isHeaderOpen = true; - } + var cellObj = row[i]; + var cell = (TableCell)cellObj; - hasAlreadyHeader = true; - } - else if (!hasBody) + renderer.EnsureLine(); + renderer.Write(row.IsHeader ? ""); - isHeaderOpen = false; - } - - renderer.WriteLine(""); - hasBody = true; + renderer.Write($" colspan=\"{cell.ColumnSpan}\""); } - renderer.Write("'); - for (int i = 0; i < row.Count; i++) + if (cell.RowSpan != 1) { - var cellObj = row[i]; - var cell = (TableCell)cellObj; - - renderer.EnsureLine(); - renderer.Write(row.IsHeader ? " 0) + if (table.ColumnDefinitions.Count > 0) + { + var columnIndex = cell.ColumnIndex < 0 || cell.ColumnIndex >= table.ColumnDefinitions.Count + ? i + : cell.ColumnIndex; + columnIndex = columnIndex >= table.ColumnDefinitions.Count ? table.ColumnDefinitions.Count - 1 : columnIndex; + var alignment = table.ColumnDefinitions[columnIndex].Alignment; + if (alignment.HasValue) { - var columnIndex = cell.ColumnIndex < 0 || cell.ColumnIndex >= table.ColumnDefinitions.Count - ? i - : cell.ColumnIndex; - columnIndex = columnIndex >= table.ColumnDefinitions.Count ? table.ColumnDefinitions.Count - 1 : columnIndex; - var alignment = table.ColumnDefinitions[columnIndex].Alignment; - if (alignment.HasValue) + switch (alignment) { - switch (alignment) - { - case TableColumnAlign.Center: - renderer.Write(" style=\"text-align: center;\""); - break; - case TableColumnAlign.Right: - renderer.Write(" style=\"text-align: right;\""); - break; - case TableColumnAlign.Left: - renderer.Write(" style=\"text-align: left;\""); - break; - } + case TableColumnAlign.Center: + renderer.Write(" style=\"text-align: center;\""); + break; + case TableColumnAlign.Right: + renderer.Write(" style=\"text-align: right;\""); + break; + case TableColumnAlign.Left: + renderer.Write(" style=\"text-align: left;\""); + break; } } + } - renderer.WriteAttributes(cell); - renderer.Write('>'); - - var previousImplicitParagraph = renderer.ImplicitParagraph; - if (cell.Count == 1) - { - renderer.ImplicitParagraph = true; - } - - renderer.Write(cell); - renderer.ImplicitParagraph = previousImplicitParagraph; + renderer.WriteAttributes(cell); + renderer.Write('>'); - renderer.WriteLine(row.IsHeader ? "" : ""); + var previousImplicitParagraph = renderer.ImplicitParagraph; + if (cell.Count == 1) + { + renderer.ImplicitParagraph = true; } - renderer.WriteLine(""); - } + renderer.Write(cell); + renderer.ImplicitParagraph = previousImplicitParagraph; - if (hasBody) - { - renderer.WriteLine(""); - } - else if (isHeaderOpen) - { - renderer.WriteLine(""); + renderer.WriteLine(row.IsHeader ? "" : ""); } - renderer.WriteLine("
    "); + renderer.WriteLine(""); } - else + + if (hasBody) { - //no html, just write the table contents - var impliciParagraph = renderer.ImplicitParagraph; + renderer.WriteLine(""); + } + else if (isHeaderOpen) + { + renderer.WriteLine(""); + } - //enable implicit paragraphs to avoid newlines after each cell - renderer.ImplicitParagraph = true; - foreach (var rowObj in table) + renderer.WriteLine(""); + } + else + { + //no html, just write the table contents + var impliciParagraph = renderer.ImplicitParagraph; + + //enable implicit paragraphs to avoid newlines after each cell + renderer.ImplicitParagraph = true; + foreach (var rowObj in table) + { + var row = (TableRow)rowObj; + for (int i = 0; i < row.Count; i++) { - var row = (TableRow)rowObj; - for (int i = 0; i < row.Count; i++) - { - var cellObj = row[i]; - var cell = (TableCell)cellObj; - renderer.Write(cell); - //write a space after each cell to avoid text being merged with the next cell - renderer.Write(' '); - } + var cellObj = row[i]; + var cell = (TableCell)cellObj; + renderer.Write(cell); + //write a space after each cell to avoid text being merged with the next cell + renderer.Write(' '); } - renderer.ImplicitParagraph = impliciParagraph; } + renderer.ImplicitParagraph = impliciParagraph; } } } diff --git a/src/Markdig/Extensions/Tables/PipeTableBlockParser.cs b/src/Markdig/Extensions/Tables/PipeTableBlockParser.cs index 858f16c61..74b75ff91 100644 --- a/src/Markdig/Extensions/Tables/PipeTableBlockParser.cs +++ b/src/Markdig/Extensions/Tables/PipeTableBlockParser.cs @@ -6,64 +6,63 @@ using Markdig.Parsers; using Markdig.Syntax; -namespace Markdig.Extensions.Tables +namespace Markdig.Extensions.Tables; + +/// +/// This block parsers for pipe tables is used to by-pass list items that could start by a single '-' +/// and would disallow to detect a pipe tables at inline parsing time, so we are basically forcing a line +/// that starts by a '-' and have at least a '|' (and have optional spaces) and is a continuation of a +/// paragraph. +/// +/// +public class PipeTableBlockParser : BlockParser { /// - /// This block parsers for pipe tables is used to by-pass list items that could start by a single '-' - /// and would disallow to detect a pipe tables at inline parsing time, so we are basically forcing a line - /// that starts by a '-' and have at least a '|' (and have optional spaces) and is a continuation of a - /// paragraph. + /// Initializes a new instance of the class. /// - /// - public class PipeTableBlockParser : BlockParser + public PipeTableBlockParser() + { + OpeningCharacters = new[] {'-'}; + } + + public override BlockState TryOpen(BlockProcessor processor) { - /// - /// Initializes a new instance of the class. - /// - public PipeTableBlockParser() + // Only if we have already a paragraph + var paragraph = processor.CurrentBlock as ParagraphBlock; + if (processor.IsCodeIndent || paragraph is null) { - OpeningCharacters = new[] {'-'}; + return BlockState.None; } - public override BlockState TryOpen(BlockProcessor processor) + // We require at least a pipe (and we allow only : - | and space characters) + var line = processor.Line; + var countPipe = 0; + while (true) { - // Only if we have already a paragraph - var paragraph = processor.CurrentBlock as ParagraphBlock; - if (processor.IsCodeIndent || paragraph is null) + var c = line.NextChar(); + if (c == '\0') { + if (countPipe > 0) + { + // Mark the paragraph as open (important, otherwise we would have an infinite loop) + paragraph.AppendLine(ref processor.Line, processor.Column, processor.LineIndex, processor.Line.Start, processor.TrackTrivia); + paragraph.IsOpen = true; + return BlockState.BreakDiscard; + } + return BlockState.None; } - - // We require at least a pipe (and we allow only : - | and space characters) - var line = processor.Line; - var countPipe = 0; - while (true) + + if (c.IsSpace() || c == '-' || c == '|' || c == ':') { - var c = line.NextChar(); - if (c == '\0') - { - if (countPipe > 0) - { - // Mark the paragraph as open (important, otherwise we would have an infinite loop) - paragraph.AppendLine(ref processor.Line, processor.Column, processor.LineIndex, processor.Line.Start, processor.TrackTrivia); - paragraph.IsOpen = true; - return BlockState.BreakDiscard; - } - - return BlockState.None; - } - - if (c.IsSpace() || c == '-' || c == '|' || c == ':') + if (c == '|') { - if (c == '|') - { - countPipe++; - } - continue; + countPipe++; } - - return BlockState.None; + continue; } + + return BlockState.None; } } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Tables/PipeTableDelimiterInline.cs b/src/Markdig/Extensions/Tables/PipeTableDelimiterInline.cs index b0865e451..60386e968 100644 --- a/src/Markdig/Extensions/Tables/PipeTableDelimiterInline.cs +++ b/src/Markdig/Extensions/Tables/PipeTableDelimiterInline.cs @@ -5,26 +5,25 @@ using Markdig.Parsers; using Markdig.Syntax.Inlines; -namespace Markdig.Extensions.Tables +namespace Markdig.Extensions.Tables; + +/// +/// The delimiter used to separate the columns of a pipe table. +/// +/// +public class PipeTableDelimiterInline : DelimiterInline { - /// - /// The delimiter used to separate the columns of a pipe table. - /// - /// - public class PipeTableDelimiterInline : DelimiterInline + public PipeTableDelimiterInline(InlineParser parser) : base(parser) { - public PipeTableDelimiterInline(InlineParser parser) : base(parser) - { - } + } - /// - /// Gets or sets the index of line where this delimiter was found relative to the current block. - /// - public int LocalLineIndex { get; set; } + /// + /// Gets or sets the index of line where this delimiter was found relative to the current block. + /// + public int LocalLineIndex { get; set; } - public override string ToLiteral() - { - return "|"; - } + public override string ToLiteral() + { + return "|"; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Tables/PipeTableExtension.cs b/src/Markdig/Extensions/Tables/PipeTableExtension.cs index 532322fd3..84ae8ca38 100644 --- a/src/Markdig/Extensions/Tables/PipeTableExtension.cs +++ b/src/Markdig/Extensions/Tables/PipeTableExtension.cs @@ -5,49 +5,48 @@ using Markdig.Parsers.Inlines; using Markdig.Renderers; -namespace Markdig.Extensions.Tables +namespace Markdig.Extensions.Tables; + +/// +/// Extension that allows to use pipe tables. +/// +/// +public class PipeTableExtension : IMarkdownExtension { /// - /// Extension that allows to use pipe tables. + /// Initializes a new instance of the class. /// - /// - public class PipeTableExtension : IMarkdownExtension + /// The options. + public PipeTableExtension(PipeTableOptions? options = null) { - /// - /// Initializes a new instance of the class. - /// - /// The options. - public PipeTableExtension(PipeTableOptions? options = null) - { - Options = options ?? new PipeTableOptions(); - } + Options = options ?? new PipeTableOptions(); + } - /// - /// Gets the options. - /// - public PipeTableOptions Options { get; } + /// + /// Gets the options. + /// + public PipeTableOptions Options { get; } - public void Setup(MarkdownPipelineBuilder pipeline) + public void Setup(MarkdownPipelineBuilder pipeline) + { + // Pipe tables require precise source location + pipeline.PreciseSourceLocation = true; + if (!pipeline.BlockParsers.Contains()) + { + pipeline.BlockParsers.Insert(0, new PipeTableBlockParser()); + } + var lineBreakParser = pipeline.InlineParsers.FindExact(); + if (!pipeline.InlineParsers.Contains()) { - // Pipe tables require precise source location - pipeline.PreciseSourceLocation = true; - if (!pipeline.BlockParsers.Contains()) - { - pipeline.BlockParsers.Insert(0, new PipeTableBlockParser()); - } - var lineBreakParser = pipeline.InlineParsers.FindExact(); - if (!pipeline.InlineParsers.Contains()) - { - pipeline.InlineParsers.InsertBefore(new PipeTableParser(lineBreakParser!, Options)); - } + pipeline.InlineParsers.InsertBefore(new PipeTableParser(lineBreakParser!, Options)); } + } - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + if (renderer is HtmlRenderer htmlRenderer && !htmlRenderer.ObjectRenderers.Contains()) { - if (renderer is HtmlRenderer htmlRenderer && !htmlRenderer.ObjectRenderers.Contains()) - { - htmlRenderer.ObjectRenderers.Add(new HtmlTableRenderer()); - } + htmlRenderer.ObjectRenderers.Add(new HtmlTableRenderer()); } } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Tables/PipeTableOptions.cs b/src/Markdig/Extensions/Tables/PipeTableOptions.cs index ffe5905c0..05181ec24 100644 --- a/src/Markdig/Extensions/Tables/PipeTableOptions.cs +++ b/src/Markdig/Extensions/Tables/PipeTableOptions.cs @@ -2,36 +2,35 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -namespace Markdig.Extensions.Tables +namespace Markdig.Extensions.Tables; + +/// +/// Options for the extension +/// +public class PipeTableOptions { /// - /// Options for the extension + /// Initializes a new instance of the class. /// - public class PipeTableOptions + public PipeTableOptions() { - /// - /// Initializes a new instance of the class. - /// - public PipeTableOptions() - { - RequireHeaderSeparator = true; - UseHeaderForColumnCount = false; - } + RequireHeaderSeparator = true; + UseHeaderForColumnCount = false; + } - /// - /// Gets or sets a value indicating whether to require header separator. true by default (Kramdown is using false) - /// - public bool RequireHeaderSeparator { get; set; } + /// + /// Gets or sets a value indicating whether to require header separator. true by default (Kramdown is using false) + /// + public bool RequireHeaderSeparator { get; set; } - /// - /// Defines whether table should be normalized to the amount of columns as defined in the table header. - /// false by default - /// - /// If true, this will insert empty cells in rows with fewer tables than the header row and remove cells - /// that are exceeding the header column count. - /// If false, this will use the row with the most columns to determine how many cells should be inserted - /// in all other rows (default behavior). - /// - public bool UseHeaderForColumnCount { get; set; } - } + /// + /// Defines whether table should be normalized to the amount of columns as defined in the table header. + /// false by default + /// + /// If true, this will insert empty cells in rows with fewer tables than the header row and remove cells + /// that are exceeding the header column count. + /// If false, this will use the row with the most columns to determine how many cells should be inserted + /// in all other rows (default behavior). + /// + public bool UseHeaderForColumnCount { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Tables/PipeTableParser.cs b/src/Markdig/Extensions/Tables/PipeTableParser.cs index 1383274d0..67411e146 100644 --- a/src/Markdig/Extensions/Tables/PipeTableParser.cs +++ b/src/Markdig/Extensions/Tables/PipeTableParser.cs @@ -2,9 +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; -using System.Collections.Generic; using System.Diagnostics; + using Markdig.Helpers; using Markdig.Parsers; using Markdig.Parsers.Inlines; @@ -12,632 +11,631 @@ using Markdig.Syntax; using Markdig.Syntax.Inlines; -namespace Markdig.Extensions.Tables +namespace Markdig.Extensions.Tables; + +/// +/// The inline parser used to transform a into a at inline parsing time. +/// +/// +/// +public class PipeTableParser : InlineParser, IPostInlineProcessor { + private readonly LineBreakInlineParser lineBreakParser; + + /// + /// Initializes a new instance of the class. + /// + /// The linebreak parser to use + /// The options. + public PipeTableParser(LineBreakInlineParser lineBreakParser, PipeTableOptions? options = null) + { + this.lineBreakParser = lineBreakParser ?? throw new ArgumentNullException(nameof(lineBreakParser)); + OpeningCharacters = new[] { '|', '\n', '\r' }; + Options = options ?? new PipeTableOptions(); + } + /// - /// The inline parser used to transform a into a at inline parsing time. + /// Gets the options. /// - /// - /// - public class PipeTableParser : InlineParser, IPostInlineProcessor + public PipeTableOptions Options { get; } + + public override bool Match(InlineProcessor processor, ref StringSlice slice) { - private readonly LineBreakInlineParser lineBreakParser; - - /// - /// Initializes a new instance of the class. - /// - /// The linebreak parser to use - /// The options. - public PipeTableParser(LineBreakInlineParser lineBreakParser, PipeTableOptions? options = null) + // Only working on Paragraph block + if (!processor.Block!.IsParagraphBlock) { - this.lineBreakParser = lineBreakParser ?? throw new ArgumentNullException(nameof(lineBreakParser)); - OpeningCharacters = new[] { '|', '\n', '\r' }; - Options = options ?? new PipeTableOptions(); + return false; } - /// - /// Gets the options. - /// - public PipeTableOptions Options { get; } + var c = slice.CurrentChar; + + // If we have not a delimiter on the first line of a paragraph, don't bother to continue + // tracking other delimiters on following lines + var tableState = processor.ParserStates[Index] as TableState; + bool isFirstLineEmpty = false; + - public override bool Match(InlineProcessor processor, ref StringSlice slice) + var position = processor.GetSourcePosition(slice.Start, out int globalLineIndex, out int column); + var localLineIndex = globalLineIndex - processor.LineIndex; + + if (tableState is null) { - // Only working on Paragraph block - if (!processor.Block!.IsParagraphBlock) + + // A table could be preceded by an empty line or a line containing an inline + // that has not been added to the stack, so we consider this as a valid + // start for a table. Typically, with this, we can have an attributes {...} + // starting on the first line of a pipe table, even if the first line + // doesn't have a pipe + if (processor.Inline != null && (localLineIndex > 0 || c == '\n' || c == '\r')) { return false; } - var c = slice.CurrentChar; + if (processor.Inline is null) + { + isFirstLineEmpty = true; + } + // Else setup a table processor + tableState = new TableState(); + processor.ParserStates[Index] = tableState; + } + + if (c == '\n' || c == '\r') + { + if (!isFirstLineEmpty && !tableState.LineHasPipe) + { + tableState.IsInvalidTable = true; + } + tableState.LineHasPipe = false; + lineBreakParser.Match(processor, ref slice); + tableState.LineIndex++; + if (!isFirstLineEmpty) + { + tableState.ColumnAndLineDelimiters.Add(processor.Inline!); + tableState.EndOfLines.Add(processor.Inline!); + } + } + else + { + processor.Inline = new PipeTableDelimiterInline(this) + { + Span = new SourceSpan(position, position), + Line = globalLineIndex, + Column = column, + LocalLineIndex = localLineIndex + }; + var deltaLine = localLineIndex - tableState.LineIndex; + if (deltaLine > 0) + { + tableState.IsInvalidTable = true; + } + tableState.LineHasPipe = true; + tableState.LineIndex = localLineIndex; + slice.SkipChar(); // Skip the `|` character - // If we have not a delimiter on the first line of a paragraph, don't bother to continue - // tracking other delimiters on following lines - var tableState = processor.ParserStates[Index] as TableState; - bool isFirstLineEmpty = false; + tableState.ColumnAndLineDelimiters.Add(processor.Inline); + } + return true; + } - var position = processor.GetSourcePosition(slice.Start, out int globalLineIndex, out int column); - var localLineIndex = globalLineIndex - processor.LineIndex; + public bool PostProcess(InlineProcessor state, Inline? root, Inline? lastChild, int postInlineProcessorIndex, bool isFinalProcessing) + { + var container = root as ContainerInline; + var tableState = state.ParserStates[Index] as TableState; - if (tableState is null) + // If the delimiters are being processed by an image link, we need to transform them back to literals + if (!isFinalProcessing) + { + if (container is null || tableState is null) { + return true; + } - // A table could be preceded by an empty line or a line containing an inline - // that has not been added to the stack, so we consider this as a valid - // start for a table. Typically, with this, we can have an attributes {...} - // starting on the first line of a pipe table, even if the first line - // doesn't have a pipe - if (processor.Inline != null && (localLineIndex > 0 || c == '\n' || c == '\r')) + var child = container.LastChild; + List? delimitersToRemove = null; + + while (child != null) + { + if (child is PipeTableDelimiterInline pipeDelimiter) { - return false; + delimitersToRemove ??= new List(); + + delimitersToRemove.Add(pipeDelimiter); } - if (processor.Inline is null) + if (child == lastChild) { - isFirstLineEmpty = true; + break; } - // Else setup a table processor - tableState = new TableState(); - processor.ParserStates[Index] = tableState; + + var subContainer = child as ContainerInline; + child = subContainer?.LastChild; } - if (c == '\n' || c == '\r') + // If we have found any delimiters, transform them to literals + if (delimitersToRemove != null) { - if (!isFirstLineEmpty && !tableState.LineHasPipe) - { - tableState.IsInvalidTable = true; - } - tableState.LineHasPipe = false; - lineBreakParser.Match(processor, ref slice); - tableState.LineIndex++; - if (!isFirstLineEmpty) + bool leftIsDelimiter = false; + bool rightIsDelimiter = false; + for (int i = 0; i < delimitersToRemove.Count; i++) { - tableState.ColumnAndLineDelimiters.Add(processor.Inline!); - tableState.EndOfLines.Add(processor.Inline!); + var pipeDelimiter = delimitersToRemove[i]; + pipeDelimiter.ReplaceByLiteral(); + + // Check that the pipe that is being removed is not going to make a line without pipe delimiters + var tableDelimiters = tableState.ColumnAndLineDelimiters; + var delimiterIndex = tableDelimiters.IndexOf(pipeDelimiter); + + if (i == 0) + { + leftIsDelimiter = delimiterIndex > 0 && tableDelimiters[delimiterIndex - 1] is PipeTableDelimiterInline; + } + else if (i + 1 == delimitersToRemove.Count) + { + rightIsDelimiter = delimiterIndex + 1 < tableDelimiters.Count && + tableDelimiters[delimiterIndex + 1] is PipeTableDelimiterInline; + } + // Remove this delimiter from the table processor + tableState.ColumnAndLineDelimiters.Remove(pipeDelimiter); } - } - else - { - processor.Inline = new PipeTableDelimiterInline(this) - { - Span = new SourceSpan(position, position), - Line = globalLineIndex, - Column = column, - LocalLineIndex = localLineIndex - }; - var deltaLine = localLineIndex - tableState.LineIndex; - if (deltaLine > 0) + + // If we didn't have any delimiter before and after the delimiters we just removed, we mark the processor of the current line as no pipe + if (!leftIsDelimiter && !rightIsDelimiter) { - tableState.IsInvalidTable = true; + tableState.LineHasPipe = false; } - tableState.LineHasPipe = true; - tableState.LineIndex = localLineIndex; - slice.SkipChar(); // Skip the `|` character - - tableState.ColumnAndLineDelimiters.Add(processor.Inline); } return true; } - public bool PostProcess(InlineProcessor state, Inline? root, Inline? lastChild, int postInlineProcessorIndex, bool isFinalProcessing) - { - var container = root as ContainerInline; - var tableState = state.ParserStates[Index] as TableState; + // Remove previous state + state.ParserStates[Index] = null!; - // If the delimiters are being processed by an image link, we need to transform them back to literals - if (!isFinalProcessing) - { - if (container is null || tableState is null) - { - return true; - } + // Continue + if (tableState is null || container is null || tableState.IsInvalidTable || !tableState.LineHasPipe ) //|| tableState.LineIndex != state.LocalLineIndex) + { + return true; + } - var child = container.LastChild; - List? delimitersToRemove = null; + // Detect the header row + var delimiters = tableState.ColumnAndLineDelimiters; + // TODO: we could optimize this by merging FindHeaderRow and the cell loop + var aligns = FindHeaderRow(delimiters); - while (child != null) - { - if (child is PipeTableDelimiterInline pipeDelimiter) - { - delimitersToRemove ??= new List(); - - delimitersToRemove.Add(pipeDelimiter); - } + if (Options.RequireHeaderSeparator && aligns is null) + { + return true; + } - if (child == lastChild) - { - break; - } + var table = new Table(); - var subContainer = child as ContainerInline; - child = subContainer?.LastChild; - } + // If the current paragraph block has any attributes attached, we can copy them + var attributes = state.Block!.TryGetAttributes(); + if (attributes != null) + { + attributes.CopyTo(table.GetAttributes()); + } - // If we have found any delimiters, transform them to literals - if (delimitersToRemove != null) + state.BlockNew = table; + var cells = tableState.Cells; + cells.Clear(); + + //delimiters[0].DumpTo(state.DebugLog); + + // delimiters contain a list of `|` and `\n` delimiters + // The `|` delimiters are created as child containers. + // So the following: + // | a | b \n + // | d | e \n + // + // Will generate a tree of the following node: + // | + // a + // | + // b + // \n + // | + // d + // | + // e + // \n + // When parsing delimiters, we need to recover whether a row is of the following form: + // 0) | a | b | \n + // 1) | a | b \n + // 2) a | b \n + // 3) a | b | \n + + // If the last element is not a line break, add a line break to homogenize parsing in the next loop + var lastElement = delimiters[delimiters.Count - 1]; + if (!(lastElement is LineBreakInline)) + { + while (true) + { + if (lastElement is ContainerInline lastElementContainer) { - bool leftIsDelimiter = false; - bool rightIsDelimiter = false; - for (int i = 0; i < delimitersToRemove.Count; i++) - { - var pipeDelimiter = delimitersToRemove[i]; - pipeDelimiter.ReplaceByLiteral(); - - // Check that the pipe that is being removed is not going to make a line without pipe delimiters - var tableDelimiters = tableState.ColumnAndLineDelimiters; - var delimiterIndex = tableDelimiters.IndexOf(pipeDelimiter); - - if (i == 0) - { - leftIsDelimiter = delimiterIndex > 0 && tableDelimiters[delimiterIndex - 1] is PipeTableDelimiterInline; - } - else if (i + 1 == delimitersToRemove.Count) - { - rightIsDelimiter = delimiterIndex + 1 < tableDelimiters.Count && - tableDelimiters[delimiterIndex + 1] is PipeTableDelimiterInline; - } - // Remove this delimiter from the table processor - tableState.ColumnAndLineDelimiters.Remove(pipeDelimiter); - } - - // If we didn't have any delimiter before and after the delimiters we just removed, we mark the processor of the current line as no pipe - if (!leftIsDelimiter && !rightIsDelimiter) + var nextElement = lastElementContainer.LastChild; + if (nextElement != null) { - tableState.LineHasPipe = false; + lastElement = nextElement; + continue; } } - - return true; + break; } - // Remove previous state - state.ParserStates[Index] = null!; - - // Continue - if (tableState is null || container is null || tableState.IsInvalidTable || !tableState.LineHasPipe ) //|| tableState.LineIndex != state.LocalLineIndex) + var endOfTable = new LineBreakInline(); + // If the last element is a container, we have to add the EOL to its child + // otherwise only next sibling + if (lastElement is ContainerInline) { - return true; + ((ContainerInline)lastElement).AppendChild(endOfTable); } - - // Detect the header row - var delimiters = tableState.ColumnAndLineDelimiters; - // TODO: we could optimize this by merging FindHeaderRow and the cell loop - var aligns = FindHeaderRow(delimiters); - - if (Options.RequireHeaderSeparator && aligns is null) + else { - return true; + lastElement.InsertAfter(endOfTable); } + delimiters.Add(endOfTable); + tableState.EndOfLines.Add(endOfTable); + } - var table = new Table(); + // Cell loop + // Reconstruct the table from the delimiters + TableRow? row = null; + TableRow? firstRow = null; + for (int i = 0; i < delimiters.Count; i++) + { + var delimiter = delimiters[i]; + var pipeSeparator = delimiter as PipeTableDelimiterInline; + var isLine = delimiter is LineBreakInline; - // If the current paragraph block has any attributes attached, we can copy them - var attributes = state.Block!.TryGetAttributes(); - if (attributes != null) + if (row is null) { - attributes.CopyTo(table.GetAttributes()); + row = new TableRow(); + + firstRow ??= row; + + // If the first delimiter is a pipe and doesn't have any parent or previous sibling, for cases like: + // 0) | a | b | \n + // 1) | a | b \n + if (pipeSeparator != null && (delimiter.PreviousSibling is null || delimiter.PreviousSibling is LineBreakInline)) + { + delimiter.Remove(); + continue; + } } - state.BlockNew = table; - var cells = tableState.Cells; - cells.Clear(); - - //delimiters[0].DumpTo(state.DebugLog); - - // delimiters contain a list of `|` and `\n` delimiters - // The `|` delimiters are created as child containers. - // So the following: - // | a | b \n - // | d | e \n - // - // Will generate a tree of the following node: - // | - // a - // | - // b - // \n - // | - // d - // | - // e - // \n - // When parsing delimiters, we need to recover whether a row is of the following form: - // 0) | a | b | \n + // We need to find the beginning/ending of a cell from a right delimiter. From the delimiter 'x', we need to find a (without the delimiter start `|`) + // So we iterate back to the first pipe or line break + // x // 1) | a | b \n // 2) a | b \n - // 3) a | b | \n - - // If the last element is not a line break, add a line break to homogenize parsing in the next loop - var lastElement = delimiters[delimiters.Count - 1]; - if (!(lastElement is LineBreakInline)) + Inline? endOfCell = null; + Inline? beginOfCell = null; + var cellContentIt = delimiter; + while (true) { - while (true) + cellContentIt = cellContentIt.PreviousSibling ?? cellContentIt.Parent; + + if (cellContentIt is null || cellContentIt is LineBreakInline) { - if (lastElement is ContainerInline lastElementContainer) - { - var nextElement = lastElementContainer.LastChild; - if (nextElement != null) - { - lastElement = nextElement; - continue; - } - } break; } - var endOfTable = new LineBreakInline(); - // If the last element is a container, we have to add the EOL to its child - // otherwise only next sibling - if (lastElement is ContainerInline) + // The cell begins at the first effective child after a | or the top ContainerInline (which is not necessary to bring into the tree + it contains an invalid span calculation) + if (cellContentIt is PipeTableDelimiterInline || (cellContentIt.GetType() == typeof(ContainerInline) && cellContentIt.Parent is null )) { - ((ContainerInline)lastElement).AppendChild(endOfTable); + beginOfCell = ((ContainerInline)cellContentIt).FirstChild; + if (endOfCell is null) + { + endOfCell = beginOfCell; + } + break; } - else + + beginOfCell = cellContentIt; + if (endOfCell is null) { - lastElement.InsertAfter(endOfTable); + endOfCell = beginOfCell; } - delimiters.Add(endOfTable); - tableState.EndOfLines.Add(endOfTable); } - // Cell loop - // Reconstruct the table from the delimiters - TableRow? row = null; - TableRow? firstRow = null; - for (int i = 0; i < delimiters.Count; i++) + // If the current deilimiter is a pipe `|` OR + // the beginOfCell/endOfCell are not null and + // either they are : + // - different + // - they contain a single element, but it is not a line break (\n) or an empty/whitespace Literal. + // Then we can add a cell to the current row + if (!isLine || (beginOfCell != null && endOfCell != null && ( beginOfCell != endOfCell || !(beginOfCell is LineBreakInline || (beginOfCell is LiteralInline beingOfCellLiteral && beingOfCellLiteral.Content.IsEmptyOrWhitespace()))))) { - var delimiter = delimiters[i]; - var pipeSeparator = delimiter as PipeTableDelimiterInline; - var isLine = delimiter is LineBreakInline; - - if (row is null) + if (!isLine) { - row = new TableRow(); - - firstRow ??= row; - - // If the first delimiter is a pipe and doesn't have any parent or previous sibling, for cases like: - // 0) | a | b | \n - // 1) | a | b \n - if (pipeSeparator != null && (delimiter.PreviousSibling is null || delimiter.PreviousSibling is LineBreakInline)) - { - delimiter.Remove(); - continue; - } + // If the delimiter is a pipe, we need to remove it from the tree + // so that previous loop looking for a parent will not go further on subsequent cells + delimiter.Remove(); } - // We need to find the beginning/ending of a cell from a right delimiter. From the delimiter 'x', we need to find a (without the delimiter start `|`) - // So we iterate back to the first pipe or line break - // x - // 1) | a | b \n - // 2) a | b \n - Inline? endOfCell = null; - Inline? beginOfCell = null; - var cellContentIt = delimiter; - while (true) - { - cellContentIt = cellContentIt.PreviousSibling ?? cellContentIt.Parent; + // We trim whitespace at the beginning and ending of the cell + TrimStart(beginOfCell); + TrimEnd(endOfCell); - if (cellContentIt is null || cellContentIt is LineBreakInline) - { - break; - } + var cellContainer = new ContainerInline(); - // The cell begins at the first effective child after a | or the top ContainerInline (which is not necessary to bring into the tree + it contains an invalid span calculation) - if (cellContentIt is PipeTableDelimiterInline || (cellContentIt.GetType() == typeof(ContainerInline) && cellContentIt.Parent is null )) - { - beginOfCell = ((ContainerInline)cellContentIt).FirstChild; - if (endOfCell is null) - { - endOfCell = beginOfCell; - } - break; - } - - beginOfCell = cellContentIt; - if (endOfCell is null) + // Copy elements from beginOfCell on the first level + var cellIt = beginOfCell; + while (cellIt != null && !IsLine(cellIt) && !(cellIt is PipeTableDelimiterInline)) + { + var nextSibling = cellIt.NextSibling; + cellIt.Remove(); + if (cellContainer.Span.IsEmpty) { - endOfCell = beginOfCell; + cellContainer.Line = cellIt.Line; + cellContainer.Column = cellIt.Column; + cellContainer.Span = cellIt.Span; } + cellContainer.AppendChild(cellIt); + cellContainer.Span.End = cellIt.Span.End; + cellIt = nextSibling; } - // If the current deilimiter is a pipe `|` OR - // the beginOfCell/endOfCell are not null and - // either they are : - // - different - // - they contain a single element, but it is not a line break (\n) or an empty/whitespace Literal. - // Then we can add a cell to the current row - if (!isLine || (beginOfCell != null && endOfCell != null && ( beginOfCell != endOfCell || !(beginOfCell is LineBreakInline || (beginOfCell is LiteralInline beingOfCellLiteral && beingOfCellLiteral.Content.IsEmptyOrWhitespace()))))) + // Create the cell and add it to the pending row + var tableParagraph = new ParagraphBlock() { - if (!isLine) - { - // If the delimiter is a pipe, we need to remove it from the tree - // so that previous loop looking for a parent will not go further on subsequent cells - delimiter.Remove(); - } - - // We trim whitespace at the beginning and ending of the cell - TrimStart(beginOfCell); - TrimEnd(endOfCell); - - var cellContainer = new ContainerInline(); - - // Copy elements from beginOfCell on the first level - var cellIt = beginOfCell; - while (cellIt != null && !IsLine(cellIt) && !(cellIt is PipeTableDelimiterInline)) - { - var nextSibling = cellIt.NextSibling; - cellIt.Remove(); - if (cellContainer.Span.IsEmpty) - { - cellContainer.Line = cellIt.Line; - cellContainer.Column = cellIt.Column; - cellContainer.Span = cellIt.Span; - } - cellContainer.AppendChild(cellIt); - cellContainer.Span.End = cellIt.Span.End; - cellIt = nextSibling; - } - - // Create the cell and add it to the pending row - var tableParagraph = new ParagraphBlock() - { - Span = cellContainer.Span, - Line = cellContainer.Line, - Column = cellContainer.Column, - Inline = cellContainer - }; - - var tableCell = new TableCell() - { - Span = cellContainer.Span, - Line = cellContainer.Line, - Column = cellContainer.Column, - }; + Span = cellContainer.Span, + Line = cellContainer.Line, + Column = cellContainer.Column, + Inline = cellContainer + }; - tableCell.Add(tableParagraph); - if (row.Span.IsEmpty) - { - row.Span = cellContainer.Span; - row.Line = cellContainer.Line; - row.Column = cellContainer.Column; - } - row.Add(tableCell); - cells.Add(tableCell); - } + var tableCell = new TableCell() + { + Span = cellContainer.Span, + Line = cellContainer.Line, + Column = cellContainer.Column, + }; - // If we have a new line, we can add the row - if (isLine) + tableCell.Add(tableParagraph); + if (row.Span.IsEmpty) { - Debug.Assert(row != null); - if (table.Span.IsEmpty) - { - table.Span = row!.Span; - table.Line = row.Line; - table.Column = row.Column; - } - table.Add(row!); - row = null; + row.Span = cellContainer.Span; + row.Line = cellContainer.Line; + row.Column = cellContainer.Column; } + row.Add(tableCell); + cells.Add(tableCell); } - // Once we are done with the cells, we can remove all end of lines in the table tree - foreach (var endOfLine in tableState.EndOfLines) + // If we have a new line, we can add the row + if (isLine) { - endOfLine.Remove(); + Debug.Assert(row != null); + if (table.Span.IsEmpty) + { + table.Span = row!.Span; + table.Line = row.Line; + table.Column = row.Column; + } + table.Add(row!); + row = null; } + } - // If we have a header row, we can remove it - // TODO: we could optimize this by merging FindHeaderRow and the previous loop - var tableRow = (TableRow)table[0]; - tableRow.IsHeader = Options.RequireHeaderSeparator; - if (aligns != null) - { - tableRow.IsHeader = true; - table.RemoveAt(1); - table.ColumnDefinitions.AddRange(aligns); - } + // Once we are done with the cells, we can remove all end of lines in the table tree + foreach (var endOfLine in tableState.EndOfLines) + { + endOfLine.Remove(); + } - // Perform delimiter processor that are coming after this processor - foreach (var cell in cells) - { - var paragraph = (ParagraphBlock) cell[0]; - state.PostProcessInlines(postInlineProcessorIndex + 1, paragraph.Inline, null, true); - } + // If we have a header row, we can remove it + // TODO: we could optimize this by merging FindHeaderRow and the previous loop + var tableRow = (TableRow)table[0]; + tableRow.IsHeader = Options.RequireHeaderSeparator; + if (aligns != null) + { + tableRow.IsHeader = true; + table.RemoveAt(1); + table.ColumnDefinitions.AddRange(aligns); + } - // Clear cells when we are done - cells.Clear(); + // Perform delimiter processor that are coming after this processor + foreach (var cell in cells) + { + var paragraph = (ParagraphBlock) cell[0]; + state.PostProcessInlines(postInlineProcessorIndex + 1, paragraph.Inline, null, true); + } - // Normalize the table - if (Options.UseHeaderForColumnCount) - { - table.NormalizeUsingHeaderRow(); - } - else - { - table.NormalizeUsingMaxWidth(); - } + // Clear cells when we are done + cells.Clear(); + + // Normalize the table + if (Options.UseHeaderForColumnCount) + { + table.NormalizeUsingHeaderRow(); + } + else + { + table.NormalizeUsingMaxWidth(); + } + + // We don't want to continue procesing delimiters, as we are already processing them here + return false; + } - // We don't want to continue procesing delimiters, as we are already processing them here + private static bool ParseHeaderString(Inline? inline, out TableColumnAlign? align) + { + align = 0; + var literal = inline as LiteralInline; + if (literal is null) + { return false; } - private static bool ParseHeaderString(Inline? inline, out TableColumnAlign? align) + // Work on a copy of the slice + var line = literal.Content; + if (TableHelper.ParseColumnHeader(ref line, '-', out align)) { - align = 0; - var literal = inline as LiteralInline; - if (literal is null) + if (line.CurrentChar != '\0') { return false; } + return true; + } - // Work on a copy of the slice - var line = literal.Content; - if (TableHelper.ParseColumnHeader(ref line, '-', out align)) + return false; + } + + private List? FindHeaderRow(List delimiters) + { + bool isValidRow = false; + List? aligns = null; + for (int i = 0; i < delimiters.Count; i++) + { + if (!IsLine(delimiters[i])) { - if (line.CurrentChar != '\0') - { - return false; - } - return true; + continue; } - return false; - } - - private List? FindHeaderRow(List delimiters) - { - bool isValidRow = false; - List? aligns = null; - for (int i = 0; i < delimiters.Count; i++) + // The last delimiter is always null, + for (int j = i + 1; j < delimiters.Count; j++) { - if (!IsLine(delimiters[i])) + var delimiter = delimiters[j]; + var nextDelimiter = j + 1 < delimiters.Count ? delimiters[j + 1] : null; + + var columnDelimiter = delimiter as PipeTableDelimiterInline; + if (j == i + 1 && IsStartOfLineColumnDelimiter(columnDelimiter)) { continue; } - // The last delimiter is always null, - for (int j = i + 1; j < delimiters.Count; j++) + // Check the left side of a `|` delimiter + TableColumnAlign? align = null; + if (delimiter.PreviousSibling != null && + !(delimiter.PreviousSibling is LiteralInline li && li.Content.IsEmptyOrWhitespace()) && // ignore parsed whitespace + !ParseHeaderString(delimiter.PreviousSibling, out align)) { - var delimiter = delimiters[j]; - var nextDelimiter = j + 1 < delimiters.Count ? delimiters[j + 1] : null; - - var columnDelimiter = delimiter as PipeTableDelimiterInline; - if (j == i + 1 && IsStartOfLineColumnDelimiter(columnDelimiter)) - { - continue; - } + break; + } - // Check the left side of a `|` delimiter - TableColumnAlign? align = null; - if (delimiter.PreviousSibling != null && - !(delimiter.PreviousSibling is LiteralInline li && li.Content.IsEmptyOrWhitespace()) && // ignore parsed whitespace - !ParseHeaderString(delimiter.PreviousSibling, out align)) - { - break; - } + // Create aligns until we may have a header row - // Create aligns until we may have a header row + aligns ??= new List(); + + aligns.Add(new TableColumnDefinition() { Alignment = align }); - aligns ??= new List(); - - aligns.Add(new TableColumnDefinition() { Alignment = align }); + // If this is the last delimiter, we need to check the right side of the `|` delimiter + if (nextDelimiter is null) + { + var nextSibling = columnDelimiter != null + ? columnDelimiter.FirstChild + : delimiter.NextSibling; - // If this is the last delimiter, we need to check the right side of the `|` delimiter - if (nextDelimiter is null) + // If there is no content after + if (IsNullOrSpace(nextSibling)) { - var nextSibling = columnDelimiter != null - ? columnDelimiter.FirstChild - : delimiter.NextSibling; - - // If there is no content after - if (IsNullOrSpace(nextSibling)) - { - isValidRow = true; - break; - } - - if (!ParseHeaderString(nextSibling, out align)) - { - break; - } - isValidRow = true; - aligns.Add(new TableColumnDefinition() { Alignment = align }); break; } - // If we are on a Line delimiter, exit - if (IsLine(delimiter)) + if (!ParseHeaderString(nextSibling, out align)) { - isValidRow = true; break; } + + isValidRow = true; + aligns.Add(new TableColumnDefinition() { Alignment = align }); + break; + } + + // If we are on a Line delimiter, exit + if (IsLine(delimiter)) + { + isValidRow = true; + break; } - break; } + break; + } + + return isValidRow ? aligns : null; + } + + private static bool IsLine(Inline inline) + { + return inline is LineBreakInline; + } - return isValidRow ? aligns : null; + private static bool IsStartOfLineColumnDelimiter(Inline? inline) + { + if (inline is null) + { + return false; } - private static bool IsLine(Inline inline) + var previous = inline.PreviousSibling; + if (previous is null) { - return inline is LineBreakInline; + return true; } - private static bool IsStartOfLineColumnDelimiter(Inline? inline) + if (previous is LiteralInline literal) { - if (inline is null) + if (!literal.Content.IsEmptyOrWhitespace()) { return false; } - - var previous = inline.PreviousSibling; - if (previous is null) - { - return true; - } - - if (previous is LiteralInline literal) - { - if (!literal.Content.IsEmptyOrWhitespace()) - { - return false; - } - previous = previous.PreviousSibling; - } - return previous is null || IsLine(previous); + previous = previous.PreviousSibling; } + return previous is null || IsLine(previous); + } - private static void TrimStart(Inline? inline) + private static void TrimStart(Inline? inline) + { + while (inline is ContainerInline && !(inline is DelimiterInline)) { - while (inline is ContainerInline && !(inline is DelimiterInline)) - { - inline = ((ContainerInline)inline).FirstChild; - } - - if (inline is LiteralInline literal) - { - literal.Content.TrimStart(); - } + inline = ((ContainerInline)inline).FirstChild; } - private static void TrimEnd(Inline? inline) + if (inline is LiteralInline literal) { - if (inline is LiteralInline literal) - { - literal.Content.TrimEnd(); - } + literal.Content.TrimStart(); } + } - private static bool IsNullOrSpace(Inline? inline) + private static void TrimEnd(Inline? inline) + { + if (inline is LiteralInline literal) { - if (inline is null) - { - return true; - } + literal.Content.TrimEnd(); + } + } - if (inline is LiteralInline literal) - { - return literal.Content.IsEmptyOrWhitespace(); - } - return false; + private static bool IsNullOrSpace(Inline? inline) + { + if (inline is null) + { + return true; } - private sealed class TableState + if (inline is LiteralInline literal) { - public bool IsInvalidTable { get; set; } + return literal.Content.IsEmptyOrWhitespace(); + } + return false; + } - public bool LineHasPipe { get; set; } + private sealed class TableState + { + public bool IsInvalidTable { get; set; } - public int LineIndex { get; set; } + public bool LineHasPipe { get; set; } - public List ColumnAndLineDelimiters { get; } = new(); + public int LineIndex { get; set; } - public List Cells { get; } = new(); + public List ColumnAndLineDelimiters { get; } = new(); - public List EndOfLines { get; } = new(); - } + public List Cells { get; } = new(); + + public List EndOfLines { get; } = new(); } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Tables/Table.cs b/src/Markdig/Extensions/Tables/Table.cs index 827db2426..6352d8ab8 100644 --- a/src/Markdig/Extensions/Tables/Table.cs +++ b/src/Markdig/Extensions/Tables/Table.cs @@ -2,136 +2,134 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using System.Collections.Generic; using Markdig.Parsers; using Markdig.Syntax; -namespace Markdig.Extensions.Tables +namespace Markdig.Extensions.Tables; + +/// +/// Defines a table that contains an optional . +/// +/// +public class Table : ContainerBlock { /// - /// Defines a table that contains an optional . + /// Initializes a new instance of the class. /// - /// - public class Table : ContainerBlock + public Table() : base(null) { - /// - /// Initializes a new instance of the class. - /// - public Table() : base(null) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// The parser used to create this block. - public Table(BlockParser? parser) : base(parser) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The parser used to create this block. + public Table(BlockParser? parser) : base(parser) + { + } - /// - /// Gets or sets the column alignments. May be null. - /// - public List ColumnDefinitions { get; } = new(); + /// + /// Gets or sets the column alignments. May be null. + /// + public List ColumnDefinitions { get; } = new(); - /// - /// Checks if the table structure is valid. - /// - /// True if the table has rows and the number of cells per row is correct, other wise false. - public bool IsValid() + /// + /// Checks if the table structure is valid. + /// + /// True if the table has rows and the number of cells per row is correct, other wise false. + public bool IsValid() + { + // A table with no rows is not valid. + if (Count == 0) { - // A table with no rows is not valid. - if (Count == 0) - { - return false; - } - var columnCount = ColumnDefinitions.Count; - var rows = new int[Count]; - for (int i = 0; i < Count; i++) + return false; + } + var columnCount = ColumnDefinitions.Count; + var rows = new int[Count]; + for (int i = 0; i < Count; i++) + { + var row = (TableRow)this[i]; + for (int j = 0; j < row.Count; j++) { - var row = (TableRow)this[i]; - for (int j = 0; j < row.Count; j++) + var cell = (TableCell)row[j]; + rows[i] += cell.ColumnSpan; + var rowSpan = cell.RowSpan - 1; + while (rowSpan > 0) { - var cell = (TableCell)row[j]; - rows[i] += cell.ColumnSpan; - var rowSpan = cell.RowSpan - 1; - while (rowSpan > 0) + if (i+rowSpan > (rows.Length-1)) { - if (i+rowSpan > (rows.Length-1)) - { - return false; - } - - rows[i + rowSpan] += cell.ColumnSpan; - rowSpan--; + return false; } - } - if (rows[i] > columnCount) - { - return false; + + rows[i + rowSpan] += cell.ColumnSpan; + rowSpan--; } } - return true; + if (rows[i] > columnCount) + { + return false; + } } + return true; + } - /// - /// Normalizes the number of columns of this table by taking the maximum columns and appending empty cells. - /// - public void NormalizeUsingMaxWidth() + /// + /// Normalizes the number of columns of this table by taking the maximum columns and appending empty cells. + /// + public void NormalizeUsingMaxWidth() + { + var maxColumn = 0; + for (int i = 0; i < this.Count; i++) { - var maxColumn = 0; - for (int i = 0; i < this.Count; i++) + if (this[i] is TableRow row && row.Count > maxColumn) { - if (this[i] is TableRow row && row.Count > maxColumn) - { - maxColumn = row.Count; - } + maxColumn = row.Count; } + } - for (int i = 0; i < this.Count; i++) + for (int i = 0; i < this.Count; i++) + { + if (this[i] is TableRow row) { - if (this[i] is TableRow row) + for (int j = row.Count; j < maxColumn; j++) { - for (int j = row.Count; j < maxColumn; j++) - { - row.Add(new TableCell()); - } + row.Add(new TableCell()); } } } + } - /// - /// Normalizes the number of columns of this table by taking the amount of columns defined in the header - /// and appending empty cells or removing extra cells as needed. - /// - public void NormalizeUsingHeaderRow() + /// + /// Normalizes the number of columns of this table by taking the amount of columns defined in the header + /// and appending empty cells or removing extra cells as needed. + /// + public void NormalizeUsingHeaderRow() + { + if (this.Count == 0) { - if (this.Count == 0) - { - return; - } + return; + } - var maxColumn = 0; + var maxColumn = 0; - var headerRow = this[0] as TableRow; - if (headerRow != null) - { - maxColumn = headerRow.Count; - } + var headerRow = this[0] as TableRow; + if (headerRow != null) + { + maxColumn = headerRow.Count; + } - for (int i = 0; i < this.Count; i++) + for (int i = 0; i < this.Count; i++) + { + if (this[i] is TableRow row) { - if (this[i] is TableRow row) + for (int j = row.Count; j < maxColumn; j++) { - for (int j = row.Count; j < maxColumn; j++) - { - row.Add(new TableCell()); - } + row.Add(new TableCell()); + } - for (int j = maxColumn; j < row.Count; j++) - { - row.RemoveAt(j); - } + for (int j = maxColumn; j < row.Count; j++) + { + row.RemoveAt(j); } } } diff --git a/src/Markdig/Extensions/Tables/TableCell.cs b/src/Markdig/Extensions/Tables/TableCell.cs index 0855bbd46..ec096b1b5 100644 --- a/src/Markdig/Extensions/Tables/TableCell.cs +++ b/src/Markdig/Extensions/Tables/TableCell.cs @@ -5,51 +5,50 @@ using Markdig.Parsers; using Markdig.Syntax; -namespace Markdig.Extensions.Tables +namespace Markdig.Extensions.Tables; + +/// +/// Defines a cell in a +/// +/// +public class TableCell : ContainerBlock { /// - /// Defines a cell in a + /// Initializes a new instance of the class. /// - /// - public class TableCell : ContainerBlock + public TableCell() : this(null) { - /// - /// Initializes a new instance of the class. - /// - public TableCell() : this(null) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The parser used to create this block. - public TableCell(BlockParser? parser) : base(parser) - { - AllowClose = true; - ColumnSpan = 1; - ColumnIndex = -1; - RowSpan = 1; - } - - /// - /// Gets or sets the index of the column to which this cell belongs. - /// - public int ColumnIndex { get; set; } - - /// - /// Gets or sets the column span this cell is covering. Default is 1. - /// - public int ColumnSpan { get; set; } - - /// - /// Gets or sets the row span this cell is covering. Default is 1. - /// - public int RowSpan { get; set; } - - /// - /// Gets or sets whether this cell can be closed. - /// - public bool AllowClose { get; set; } } + + /// + /// Initializes a new instance of the class. + /// + /// The parser used to create this block. + public TableCell(BlockParser? parser) : base(parser) + { + AllowClose = true; + ColumnSpan = 1; + ColumnIndex = -1; + RowSpan = 1; + } + + /// + /// Gets or sets the index of the column to which this cell belongs. + /// + public int ColumnIndex { get; set; } + + /// + /// Gets or sets the column span this cell is covering. Default is 1. + /// + public int ColumnSpan { get; set; } + + /// + /// Gets or sets the row span this cell is covering. Default is 1. + /// + public int RowSpan { get; set; } + + /// + /// Gets or sets whether this cell can be closed. + /// + public bool AllowClose { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Tables/TableColumnAlign.cs b/src/Markdig/Extensions/Tables/TableColumnAlign.cs index ad67b853a..9dd563c66 100644 --- a/src/Markdig/Extensions/Tables/TableColumnAlign.cs +++ b/src/Markdig/Extensions/Tables/TableColumnAlign.cs @@ -2,26 +2,25 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -namespace Markdig.Extensions.Tables +namespace Markdig.Extensions.Tables; + +/// +/// Defines the alignment of a column +/// +public enum TableColumnAlign { /// - /// Defines the alignment of a column + /// Align the column to the left /// - public enum TableColumnAlign - { - /// - /// Align the column to the left - /// - Left, + Left, - /// - /// Align the column to the center - /// - Center, + /// + /// Align the column to the center + /// + Center, - /// - /// Align the column to the right - /// - Right, - } + /// + /// Align the column to the right + /// + Right, } \ No newline at end of file diff --git a/src/Markdig/Extensions/Tables/TableColumnDefinition.cs b/src/Markdig/Extensions/Tables/TableColumnDefinition.cs index feeb11bab..85a54d45d 100644 --- a/src/Markdig/Extensions/Tables/TableColumnDefinition.cs +++ b/src/Markdig/Extensions/Tables/TableColumnDefinition.cs @@ -2,21 +2,20 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -namespace Markdig.Extensions.Tables +namespace Markdig.Extensions.Tables; + +/// +/// Defines a column. +/// +public class TableColumnDefinition { /// - /// Defines a column. + /// Gets or sets the width (in percentage) of this column. A value of 0 is unspecified. /// - public class TableColumnDefinition - { - /// - /// Gets or sets the width (in percentage) of this column. A value of 0 is unspecified. - /// - public float Width { get; set; } + public float Width { get; set; } - /// - /// Gets or sets the column alignment. - /// - public TableColumnAlign? Alignment { get; set; } - } + /// + /// Gets or sets the column alignment. + /// + public TableColumnAlign? Alignment { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Tables/TableHelper.cs b/src/Markdig/Extensions/Tables/TableHelper.cs index 3526e1e2d..bbbda511d 100644 --- a/src/Markdig/Extensions/Tables/TableHelper.cs +++ b/src/Markdig/Extensions/Tables/TableHelper.cs @@ -4,103 +4,102 @@ using Markdig.Helpers; -namespace Markdig.Extensions.Tables +namespace Markdig.Extensions.Tables; + +/// +/// Helper methods for parsing tables. +/// +public static class TableHelper { /// - /// Helper methods for parsing tables. + /// Parses a column header equivalent to the regexp: \s*:\s*[delimiterChar]+\s*:\s* /// - public static class TableHelper + /// The text slice. + /// The delimiter character (either `-` or `=`). + /// The alignment of the column. + /// + /// true if parsing was successful + /// + public static bool ParseColumnHeader(ref StringSlice slice, char delimiterChar, out TableColumnAlign? align) { - /// - /// Parses a column header equivalent to the regexp: \s*:\s*[delimiterChar]+\s*:\s* - /// - /// The text slice. - /// The delimiter character (either `-` or `=`). - /// The alignment of the column. - /// - /// true if parsing was successfull - /// - public static bool ParseColumnHeader(ref StringSlice slice, char delimiterChar, out TableColumnAlign? align) - { - return ParseColumnHeaderDetect(ref slice, ref delimiterChar, out align); - } + return ParseColumnHeaderDetect(ref slice, ref delimiterChar, out align); + } - /// - /// Parses a column header equivalent to the regexp: \s*:\s*[delimiterChar]+\s*:\s* - /// - /// The text slice. - /// The delimiter character (either `-` or `=`). - /// The alignment of the column. - /// - /// true if parsing was successfull - /// - public static bool ParseColumnHeaderAuto(ref StringSlice slice, out char delimiterChar, out TableColumnAlign? align) - { - delimiterChar = '\0'; - return ParseColumnHeaderDetect(ref slice, ref delimiterChar, out align); - } + /// + /// Parses a column header equivalent to the regexp: \s*:\s*[delimiterChar]+\s*:\s* + /// + /// The text slice. + /// The delimiter character (either `-` or `=`). + /// The alignment of the column. + /// + /// true if parsing was successful + /// + public static bool ParseColumnHeaderAuto(ref StringSlice slice, out char delimiterChar, out TableColumnAlign? align) + { + delimiterChar = '\0'; + return ParseColumnHeaderDetect(ref slice, ref delimiterChar, out align); + } - /// - /// Parses a column header equivalent to the regexp: \s*:\s*[delimiterChar]+\s*:\s* - /// - /// The text slice. - /// The delimiter character (either `-` or `=`). If `\0`, it will detect the character (either `-` or `=`) - /// The alignment of the column. - /// - /// true if parsing was successfull - /// - public static bool ParseColumnHeaderDetect(ref StringSlice slice, ref char delimiterChar, out TableColumnAlign? align) - { - align = null; + /// + /// Parses a column header equivalent to the regexp: \s*:\s*[delimiterChar]+\s*:\s* + /// + /// The text slice. + /// The delimiter character (either `-` or `=`). If `\0`, it will detect the character (either `-` or `=`) + /// The alignment of the column. + /// + /// true if parsing was successful + /// + public static bool ParseColumnHeaderDetect(ref StringSlice slice, ref char delimiterChar, out TableColumnAlign? align) + { + align = null; - slice.TrimStart(); - var c = slice.CurrentChar; - bool hasLeft = false; - bool hasRight = false; - if (c == ':') - { - hasLeft = true; - slice.SkipChar(); - } + slice.TrimStart(); + var c = slice.CurrentChar; + bool hasLeft = false; + bool hasRight = false; + if (c == ':') + { + hasLeft = true; + slice.SkipChar(); + } - slice.TrimStart(); - c = slice.CurrentChar; + slice.TrimStart(); + c = slice.CurrentChar; - // if we want to automatically detect - if (delimiterChar == '\0') + // if we want to automatically detect + if (delimiterChar == '\0') + { + if (c == '=' || c == '-') { - if (c == '=' || c == '-') - { - delimiterChar = c; - } - else - { - return false; - } + delimiterChar = c; } - - // We expect at least one `-` delimiter char - if (slice.CountAndSkipChar(delimiterChar) == 0) + else { return false; } + } - slice.TrimStart(); - c = slice.CurrentChar; - - if (c == ':') - { - hasRight = true; - slice.SkipChar(); - } - slice.TrimStart(); + // We expect at least one `-` delimiter char + if (slice.CountAndSkipChar(delimiterChar) == 0) + { + return false; + } - align = hasLeft && hasRight - ? TableColumnAlign.Center - : hasRight ? TableColumnAlign.Right : hasLeft ? TableColumnAlign.Left : (TableColumnAlign?) null; + slice.TrimStart(); + c = slice.CurrentChar; - return true; + if (c == ':') + { + hasRight = true; + slice.SkipChar(); } + slice.TrimStart(); + + align = hasLeft && hasRight + ? TableColumnAlign.Center + : hasRight ? TableColumnAlign.Right : hasLeft ? TableColumnAlign.Left : (TableColumnAlign?) null; + return true; } + } \ No newline at end of file diff --git a/src/Markdig/Extensions/Tables/TableRow.cs b/src/Markdig/Extensions/Tables/TableRow.cs index 2f82e6bf1..cf453dd72 100644 --- a/src/Markdig/Extensions/Tables/TableRow.cs +++ b/src/Markdig/Extensions/Tables/TableRow.cs @@ -4,24 +4,23 @@ using Markdig.Syntax; -namespace Markdig.Extensions.Tables +namespace Markdig.Extensions.Tables; + +/// +/// Defines a row in a , contains , parent is . +/// +/// +public class TableRow : ContainerBlock { /// - /// Defines a row in a , contains , parent is . + /// Initializes a new instance of the class. /// - /// - public class TableRow : ContainerBlock + public TableRow() : base(null) { - /// - /// Initializes a new instance of the class. - /// - public TableRow() : base(null) - { - } - - /// - /// Gets or sets a value indicating whether this instance is header row. - /// - public bool IsHeader { get; set; } } + + /// + /// Gets or sets a value indicating whether this instance is header row. + /// + public bool IsHeader { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/TaskLists/HtmlTaskListRenderer.cs b/src/Markdig/Extensions/TaskLists/HtmlTaskListRenderer.cs index a6ad57100..24ffa0a77 100644 --- a/src/Markdig/Extensions/TaskLists/HtmlTaskListRenderer.cs +++ b/src/Markdig/Extensions/TaskLists/HtmlTaskListRenderer.cs @@ -5,31 +5,30 @@ using Markdig.Renderers; using Markdig.Renderers.Html; -namespace Markdig.Extensions.TaskLists +namespace Markdig.Extensions.TaskLists; + +/// +/// A HTML renderer for a . +/// +/// +public class HtmlTaskListRenderer : HtmlObjectRenderer { - /// - /// A HTML renderer for a . - /// - /// - public class HtmlTaskListRenderer : HtmlObjectRenderer + protected override void Write(HtmlRenderer renderer, TaskList obj) { - protected override void Write(HtmlRenderer renderer, TaskList obj) + if (renderer.EnableHtmlForInline) { - if (renderer.EnableHtmlForInline) - { - renderer.Write(""); - } - else + renderer.Write(""); + } + else + { + renderer.Write('['); + renderer.Write(obj.Checked ? "x" : " "); + renderer.Write(']'); } } } \ No newline at end of file diff --git a/src/Markdig/Extensions/TaskLists/NormalizeTaskListRenderer.cs b/src/Markdig/Extensions/TaskLists/NormalizeTaskListRenderer.cs index 5f8aeb411..e33e92a77 100644 --- a/src/Markdig/Extensions/TaskLists/NormalizeTaskListRenderer.cs +++ b/src/Markdig/Extensions/TaskLists/NormalizeTaskListRenderer.cs @@ -4,15 +4,14 @@ using Markdig.Renderers.Normalize; -namespace Markdig.Extensions.TaskLists +namespace Markdig.Extensions.TaskLists; + +public class NormalizeTaskListRenderer : NormalizeObjectRenderer { - public class NormalizeTaskListRenderer : NormalizeObjectRenderer + protected override void Write(NormalizeRenderer renderer, TaskList obj) { - protected override void Write(NormalizeRenderer renderer, TaskList obj) - { - renderer.Write("["); - renderer.Write(obj.Checked ? "X" : " "); - renderer.Write("]"); - } + renderer.Write("["); + renderer.Write(obj.Checked ? "X" : " "); + renderer.Write("]"); } } diff --git a/src/Markdig/Extensions/TaskLists/TaskList.cs b/src/Markdig/Extensions/TaskLists/TaskList.cs index 9fb548300..d917bb2d7 100644 --- a/src/Markdig/Extensions/TaskLists/TaskList.cs +++ b/src/Markdig/Extensions/TaskLists/TaskList.cs @@ -5,14 +5,13 @@ using System.Diagnostics; using Markdig.Syntax.Inlines; -namespace Markdig.Extensions.TaskLists +namespace Markdig.Extensions.TaskLists; + +/// +/// An inline for TaskList. +/// +[DebuggerDisplay("TaskList {Checked}")] +public class TaskList : LeafInline { - /// - /// An inline for TaskList. - /// - [DebuggerDisplay("TaskList {Checked}")] - public class TaskList : LeafInline - { - public bool Checked { get; set; } - } + public bool Checked { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/TaskLists/TaskListExtension.cs b/src/Markdig/Extensions/TaskLists/TaskListExtension.cs index 89e23d5b9..b81b64993 100644 --- a/src/Markdig/Extensions/TaskLists/TaskListExtension.cs +++ b/src/Markdig/Extensions/TaskLists/TaskListExtension.cs @@ -6,33 +6,32 @@ using Markdig.Renderers; using Markdig.Renderers.Normalize; -namespace Markdig.Extensions.TaskLists +namespace Markdig.Extensions.TaskLists; + +/// +/// Extension to enable TaskList. +/// +public class TaskListExtension : IMarkdownExtension { - /// - /// Extension to enable TaskList. - /// - public class TaskListExtension : IMarkdownExtension + public void Setup(MarkdownPipelineBuilder pipeline) { - public void Setup(MarkdownPipelineBuilder pipeline) + if (!pipeline.InlineParsers.Contains()) { - if (!pipeline.InlineParsers.Contains()) - { - // Insert the parser after the code span parser - pipeline.InlineParsers.InsertBefore(new TaskListInlineParser()); - } + // Insert the parser after the code span parser + pipeline.InlineParsers.InsertBefore(new TaskListInlineParser()); } + } - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + if (renderer is HtmlRenderer htmlRenderer) { - if (renderer is HtmlRenderer htmlRenderer) - { - htmlRenderer.ObjectRenderers.AddIfNotAlready(); - } + htmlRenderer.ObjectRenderers.AddIfNotAlready(); + } - if (renderer is NormalizeRenderer normalizeRenderer) - { - normalizeRenderer.ObjectRenderers.AddIfNotAlready(); - } + if (renderer is NormalizeRenderer normalizeRenderer) + { + normalizeRenderer.ObjectRenderers.AddIfNotAlready(); } } } \ No newline at end of file diff --git a/src/Markdig/Extensions/TaskLists/TaskListInlineParser.cs b/src/Markdig/Extensions/TaskLists/TaskListInlineParser.cs index faf7abdf0..a4fe4c164 100644 --- a/src/Markdig/Extensions/TaskLists/TaskListInlineParser.cs +++ b/src/Markdig/Extensions/TaskLists/TaskListInlineParser.cs @@ -7,81 +7,80 @@ using Markdig.Renderers.Html; using Markdig.Syntax; -namespace Markdig.Extensions.TaskLists +namespace Markdig.Extensions.TaskLists; + +/// +/// The inline parser for SmartyPants. +/// +public class TaskListInlineParser : InlineParser { /// - /// The inline parser for SmartyPants. + /// Initializes a new instance of the class. /// - public class TaskListInlineParser : InlineParser + public TaskListInlineParser() { - /// - /// Initializes a new instance of the class. - /// - public TaskListInlineParser() - { - OpeningCharacters = new[] {'['}; - ListClass = "contains-task-list"; - ListItemClass = "task-list-item"; - } - - /// - /// Gets or sets the list class used for a task list. - /// - public string ListClass { get; set; } + OpeningCharacters = new[] {'['}; + ListClass = "contains-task-list"; + ListItemClass = "task-list-item"; + } - /// - /// Gets or sets the list item class used for a task list. - /// - public string ListItemClass { get; set; } + /// + /// Gets or sets the list class used for a task list. + /// + public string ListClass { get; set; } - public override bool Match(InlineProcessor processor, ref StringSlice slice) - { - // A tasklist is either - // [ ] - // or [x] or [X] + /// + /// Gets or sets the list item class used for a task list. + /// + public string ListItemClass { get; set; } - if (!(processor.Block!.Parent is ListItemBlock listItemBlock)) - { - return false; - } + public override bool Match(InlineProcessor processor, ref StringSlice slice) + { + // A tasklist is either + // [ ] + // or [x] or [X] - var startingPosition = slice.Start; - var c = slice.NextChar(); - if (!c.IsSpace() && c != 'x' && c != 'X') - { - return false; - } - if (slice.NextChar() != ']') - { - return false; - } - // Skip last ] - slice.SkipChar(); + if (!(processor.Block!.Parent is ListItemBlock listItemBlock)) + { + return false; + } - // Create the TaskList - var taskItem = new TaskList() - { - Span = { Start = processor.GetSourcePosition(startingPosition, out int line, out int column) }, - Line = line, - Column = column, - Checked = !c.IsSpace() - }; - taskItem.Span.End = taskItem.Span.Start + 2; - processor.Inline = taskItem; + var startingPosition = slice.Start; + var c = slice.NextChar(); + if (!c.IsSpace() && c != 'x' && c != 'X') + { + return false; + } + if (slice.NextChar() != ']') + { + return false; + } + // Skip last ] + slice.SkipChar(); - // Add proper class for task list - if (!string.IsNullOrEmpty(ListItemClass)) - { - listItemBlock.GetAttributes().AddClass(ListItemClass); - } + // Create the TaskList + var taskItem = new TaskList() + { + Span = { Start = processor.GetSourcePosition(startingPosition, out int line, out int column) }, + Line = line, + Column = column, + Checked = !c.IsSpace() + }; + taskItem.Span.End = taskItem.Span.Start + 2; + processor.Inline = taskItem; - var listBlock = (ListBlock) listItemBlock.Parent!; - if (!string.IsNullOrEmpty(ListClass)) - { - listBlock.GetAttributes().AddClass(ListClass); - } + // Add proper class for task list + if (!string.IsNullOrEmpty(ListItemClass)) + { + listItemBlock.GetAttributes().AddClass(ListItemClass); + } - return true; + var listBlock = (ListBlock) listItemBlock.Parent!; + if (!string.IsNullOrEmpty(ListClass)) + { + listBlock.GetAttributes().AddClass(ListClass); } + + return true; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/TextRenderer/ConfigureNewLineExtension.cs b/src/Markdig/Extensions/TextRenderer/ConfigureNewLineExtension.cs index 2e820df41..55fa6b7b8 100644 --- a/src/Markdig/Extensions/TextRenderer/ConfigureNewLineExtension.cs +++ b/src/Markdig/Extensions/TextRenderer/ConfigureNewLineExtension.cs @@ -4,32 +4,31 @@ using Markdig.Renderers; -namespace Markdig.Extensions.TextRenderer +namespace Markdig.Extensions.TextRenderer; + +/// +/// Extension that allows setting line-endings for any IMarkdownRenderer +/// that inherits from +/// +/// +public class ConfigureNewLineExtension : IMarkdownExtension { - /// - /// Extension that allows setting line-endings for any IMarkdownRenderer - /// that inherits from - /// - /// - public class ConfigureNewLineExtension : IMarkdownExtension - { - private readonly string newLine; + private readonly string newLine; - public ConfigureNewLineExtension(string newLine) - { - this.newLine = newLine; - } + public ConfigureNewLineExtension(string newLine) + { + this.newLine = newLine; + } - public void Setup(MarkdownPipelineBuilder pipeline) - { - } + public void Setup(MarkdownPipelineBuilder pipeline) + { + } - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + if (renderer is TextRendererBase textRenderer) { - if (renderer is TextRendererBase textRenderer) - { - textRenderer.Writer.NewLine = newLine; - } + textRenderer.Writer.NewLine = newLine; } } } diff --git a/src/Markdig/Extensions/Yaml/YamlFrontMatterBlock.cs b/src/Markdig/Extensions/Yaml/YamlFrontMatterBlock.cs index 713bcc37b..d5e6d2ced 100644 --- a/src/Markdig/Extensions/Yaml/YamlFrontMatterBlock.cs +++ b/src/Markdig/Extensions/Yaml/YamlFrontMatterBlock.cs @@ -5,20 +5,19 @@ using Markdig.Parsers; using Markdig.Syntax; -namespace Markdig.Extensions.Yaml +namespace Markdig.Extensions.Yaml; + +/// +/// A YAML frontmatter block. +/// +/// +public class YamlFrontMatterBlock : CodeBlock { /// - /// A YAML frontmatter block. + /// Initializes a new instance of the class. /// - /// - public class YamlFrontMatterBlock : CodeBlock + /// The parser. + public YamlFrontMatterBlock(BlockParser parser) : base(parser) { - /// - /// Initializes a new instance of the class. - /// - /// The parser. - public YamlFrontMatterBlock(BlockParser parser) : base(parser) - { - } } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Yaml/YamlFrontMatterExtension.cs b/src/Markdig/Extensions/Yaml/YamlFrontMatterExtension.cs index e20cb0f3a..4b1959b89 100644 --- a/src/Markdig/Extensions/Yaml/YamlFrontMatterExtension.cs +++ b/src/Markdig/Extensions/Yaml/YamlFrontMatterExtension.cs @@ -5,33 +5,32 @@ using Markdig.Parsers; using Markdig.Renderers; -namespace Markdig.Extensions.Yaml +namespace Markdig.Extensions.Yaml; + +/// +/// Extension to discard a YAML frontmatter at the beginning of a Markdown document. +/// +public class YamlFrontMatterExtension : IMarkdownExtension { - /// - /// Extension to discard a YAML frontmatter at the beginning of a Markdown document. - /// - public class YamlFrontMatterExtension : IMarkdownExtension + public void Setup(MarkdownPipelineBuilder pipeline) { - public void Setup(MarkdownPipelineBuilder pipeline) + if (!pipeline.BlockParsers.Contains()) { - if (!pipeline.BlockParsers.Contains()) - { - // Insert the YAML parser before the thematic break parser, as it is also triggered on a --- dash - pipeline.BlockParsers.InsertBefore(new YamlFrontMatterParser()); - } + // Insert the YAML parser before the thematic break parser, as it is also triggered on a --- dash + pipeline.BlockParsers.InsertBefore(new YamlFrontMatterParser()); } + } - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + if (!renderer.ObjectRenderers.Contains()) { - if (!renderer.ObjectRenderers.Contains()) - { - renderer.ObjectRenderers.InsertBefore(new YamlFrontMatterHtmlRenderer()); - } + renderer.ObjectRenderers.InsertBefore(new YamlFrontMatterHtmlRenderer()); + } - if (!renderer.ObjectRenderers.Contains()) - { - renderer.ObjectRenderers.InsertBefore(new YamlFrontMatterRoundtripRenderer()); - } + if (!renderer.ObjectRenderers.Contains()) + { + renderer.ObjectRenderers.InsertBefore(new YamlFrontMatterRoundtripRenderer()); } } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Yaml/YamlFrontMatterHtmlRenderer.cs b/src/Markdig/Extensions/Yaml/YamlFrontMatterHtmlRenderer.cs index c06493163..1e300d671 100644 --- a/src/Markdig/Extensions/Yaml/YamlFrontMatterHtmlRenderer.cs +++ b/src/Markdig/Extensions/Yaml/YamlFrontMatterHtmlRenderer.cs @@ -5,16 +5,15 @@ using Markdig.Renderers; using Markdig.Renderers.Html; -namespace Markdig.Extensions.Yaml +namespace Markdig.Extensions.Yaml; + +/// +/// Empty renderer for a +/// +/// +public class YamlFrontMatterHtmlRenderer : HtmlObjectRenderer { - /// - /// Empty renderer for a - /// - /// - public class YamlFrontMatterHtmlRenderer : HtmlObjectRenderer + protected override void Write(HtmlRenderer renderer, YamlFrontMatterBlock obj) { - protected override void Write(HtmlRenderer renderer, YamlFrontMatterBlock obj) - { - } } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Yaml/YamlFrontMatterParser.cs b/src/Markdig/Extensions/Yaml/YamlFrontMatterParser.cs index 532d2e412..3841abc55 100644 --- a/src/Markdig/Extensions/Yaml/YamlFrontMatterParser.cs +++ b/src/Markdig/Extensions/Yaml/YamlFrontMatterParser.cs @@ -6,154 +6,153 @@ using Markdig.Parsers; using Markdig.Syntax; -namespace Markdig.Extensions.Yaml +namespace Markdig.Extensions.Yaml; + +/// +/// Block parser for a YAML frontmatter. +/// +/// +public class YamlFrontMatterParser : BlockParser { + // We reuse a FencedCodeBlock parser to grab a frontmatter, only active if it happens on the first line of the document. + /// - /// Block parser for a YAML frontmatter. + /// Initializes a new instance of the class. /// - /// - public class YamlFrontMatterParser : BlockParser + public YamlFrontMatterParser() { - // We reuse a FencedCodeBlock parser to grab a frontmatter, only active if it happens on the first line of the document. + this.OpeningCharacters = new[] { '-' }; + } - /// - /// Initializes a new instance of the class. - /// - public YamlFrontMatterParser() - { - this.OpeningCharacters = new[] { '-' }; - } + /// + /// Creates the front matter block. + /// + /// The block processor + /// The front matter block + protected virtual YamlFrontMatterBlock CreateFrontMatterBlock(BlockProcessor processor) + { + return new YamlFrontMatterBlock(this); + } - /// - /// Creates the front matter block. - /// - /// The block processor - /// The front matter block - protected virtual YamlFrontMatterBlock CreateFrontMatterBlock(BlockProcessor processor) + /// + /// Tries to match a block opening. + /// + /// The parser processor. + /// The result of the match + public override BlockState TryOpen(BlockProcessor processor) + { + // We expect no indentation for a fenced code block. + if (processor.IsCodeIndent) { - return new YamlFrontMatterBlock(this); + return BlockState.None; } - /// - /// Tries to match a block opening. - /// - /// The parser processor. - /// The result of the match - public override BlockState TryOpen(BlockProcessor processor) + // Only accept a frontmatter at the beginning of the file + if (processor.Start != 0) { - // We expect no indentation for a fenced code block. - if (processor.IsCodeIndent) - { - return BlockState.None; - } - - // Only accept a frontmatter at the beginning of the file - if (processor.Start != 0) - { - return BlockState.None; - } + return BlockState.None; + } - int count = 0; - var line = processor.Line; - char c = line.CurrentChar; + int count = 0; + var line = processor.Line; + char c = line.CurrentChar; - // Must consist of exactly three dashes - while (c == '-' && count < 4) - { - count++; - c = line.NextChar(); - } + // Must consist of exactly three dashes + while (c == '-' && count < 4) + { + count++; + c = line.NextChar(); + } - // If three dashes (optionally followed by whitespace) - // this is a YAML front matter block - if (count == 3 && (c == '\0' || c.IsWhitespace()) && line.TrimEnd()) + // If three dashes (optionally followed by whitespace) + // this is a YAML front matter block + if (count == 3 && (c == '\0' || c.IsWhitespace()) && line.TrimEnd()) + { + bool hasFullYamlFrontMatter = false; + // We make sure that there is a closing frontmatter somewhere in the document + // so here we work on the full document instead of just the line + var fullLine = new StringSlice(line.Text, line.Start, line.Text.Length - 1); + c = fullLine.CurrentChar; + while (c != '\0') { - bool hasFullYamlFrontMatter = false; - // We make sure that there is a closing frontmatter somewhere in the document - // so here we work on the full document instead of just the line - var fullLine = new StringSlice(line.Text, line.Start, line.Text.Length - 1); - c = fullLine.CurrentChar; - while (c != '\0') + c = fullLine.NextChar(); + if (c == '\n' || c == '\r') { - c = fullLine.NextChar(); - if (c == '\n' || c == '\r') + var nc = fullLine.PeekChar(); + if (c == '\r' && nc == '\n') { - var nc = fullLine.PeekChar(); - if (c == '\r' && nc == '\n') - { - c = fullLine.NextChar(); - } - nc = fullLine.PeekChar(); - if (nc == '-') + c = fullLine.NextChar(); + } + nc = fullLine.PeekChar(); + if (nc == '-') + { + if (fullLine.NextChar() == '-' && fullLine.NextChar() == '-' && fullLine.NextChar() == '-' && (fullLine.NextChar() == '\0' || fullLine.SkipSpacesToEndOfLineOrEndOfDocument())) { - if (fullLine.NextChar() == '-' && fullLine.NextChar() == '-' && fullLine.NextChar() == '-' && (fullLine.NextChar() == '\0' || fullLine.SkipSpacesToEndOfLineOrEndOfDocument())) - { - hasFullYamlFrontMatter = true; - break; - } + hasFullYamlFrontMatter = true; + break; } - else if (nc == '.') + } + else if (nc == '.') + { + if (fullLine.NextChar() == '.' && fullLine.NextChar() == '.' && fullLine.NextChar() == '.' && (fullLine.NextChar() == '\0' || fullLine.SkipSpacesToEndOfLineOrEndOfDocument())) { - if (fullLine.NextChar() == '.' && fullLine.NextChar() == '.' && fullLine.NextChar() == '.' && (fullLine.NextChar() == '\0' || fullLine.SkipSpacesToEndOfLineOrEndOfDocument())) - { - hasFullYamlFrontMatter = true; - break; - } + hasFullYamlFrontMatter = true; + break; } } } + } - if (hasFullYamlFrontMatter) - { - // Create a front matter block - var block = this.CreateFrontMatterBlock(processor); - block.Column = processor.Column; - block.Span.Start = 0; - block.Span.End = line.Start; + if (hasFullYamlFrontMatter) + { + // Create a front matter block + var block = this.CreateFrontMatterBlock(processor); + block.Column = processor.Column; + block.Span.Start = 0; + block.Span.End = line.Start; - // Store the number of matched string into the context - processor.NewBlocks.Push(block); + // Store the number of matched string into the context + processor.NewBlocks.Push(block); - // Discard the current line as it is already parsed - return BlockState.ContinueDiscard; - } + // Discard the current line as it is already parsed + return BlockState.ContinueDiscard; } - - return BlockState.None; } - /// - /// Tries to continue matching a block already opened. - /// - /// The parser processor. - /// The block already opened. - /// The result of the match. By default, don't expect any newline - public override BlockState TryContinue(BlockProcessor processor, Block block) + return BlockState.None; + } + + /// + /// Tries to continue matching a block already opened. + /// + /// The parser processor. + /// The block already opened. + /// The result of the match. By default, don't expect any newline + public override BlockState TryContinue(BlockProcessor processor, Block block) + { + // Determine if we have a closing fence. + // It can start or end with either --- or ... + var line = processor.Line; + var c = line.CurrentChar; + if (processor.Column == 0 && (c == '-' || c == '.')) { - // Determine if we have a closing fence. - // It can start or end with either --- or ... - var line = processor.Line; - var c = line.CurrentChar; - if (processor.Column == 0 && (c == '-' || c == '.')) - { - int count = line.CountAndSkipChar(c); - c = line.CurrentChar; + int count = line.CountAndSkipChar(c); + c = line.CurrentChar; - // If we have a closing fence, close it and discard the current line - // The line must contain only fence characters and optional following whitespace. - if (count == 3 && !processor.IsCodeIndent && (c == '\0' || c.IsWhitespace()) && line.TrimEnd()) - { - block.UpdateSpanEnd(line.Start - 1); + // If we have a closing fence, close it and discard the current line + // The line must contain only fence characters and optional following whitespace. + if (count == 3 && !processor.IsCodeIndent && (c == '\0' || c.IsWhitespace()) && line.TrimEnd()) + { + block.UpdateSpanEnd(line.Start - 1); - // Don't keep the last line - return BlockState.BreakDiscard; - } + // Don't keep the last line + return BlockState.BreakDiscard; } + } - // Reset the indentation to the column before the indent - processor.GoToColumn(processor.ColumnBeforeIndent); + // Reset the indentation to the column before the indent + processor.GoToColumn(processor.ColumnBeforeIndent); - return BlockState.Continue; - } + return BlockState.Continue; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Yaml/YamlFrontMatterRoundtripRenderer.cs b/src/Markdig/Extensions/Yaml/YamlFrontMatterRoundtripRenderer.cs index ed542fc62..b87633bdf 100644 --- a/src/Markdig/Extensions/Yaml/YamlFrontMatterRoundtripRenderer.cs +++ b/src/Markdig/Extensions/Yaml/YamlFrontMatterRoundtripRenderer.cs @@ -2,26 +2,24 @@ // 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 Markdig.Renderers; using Markdig.Renderers.Roundtrip; -namespace Markdig.Extensions.Yaml +namespace Markdig.Extensions.Yaml; + +public class YamlFrontMatterRoundtripRenderer : MarkdownObjectRenderer { - public class YamlFrontMatterRoundtripRenderer : MarkdownObjectRenderer - { - private readonly CodeBlockRenderer _codeBlockRenderer; + private readonly CodeBlockRenderer _codeBlockRenderer; - public YamlFrontMatterRoundtripRenderer() - { - _codeBlockRenderer = new CodeBlockRenderer(); - } + public YamlFrontMatterRoundtripRenderer() + { + _codeBlockRenderer = new CodeBlockRenderer(); + } - protected override void Write(RoundtripRenderer renderer, YamlFrontMatterBlock obj) - { - renderer.Writer.WriteLine("---"); - _codeBlockRenderer.Write(renderer, obj); - renderer.Writer.WriteLine("---"); - } + protected override void Write(RoundtripRenderer renderer, YamlFrontMatterBlock obj) + { + renderer.Writer.WriteLine("---"); + _codeBlockRenderer.Write(renderer, obj); + renderer.Writer.WriteLine("---"); } } \ No newline at end of file diff --git a/src/Markdig/Globals.cs b/src/Markdig/Globals.cs new file mode 100644 index 000000000..1a8b91ba5 --- /dev/null +++ b/src/Markdig/Globals.cs @@ -0,0 +1,2 @@ +global using System; +global using System.Collections.Generic; \ No newline at end of file diff --git a/src/Markdig/Helpers/BlockWrapper.cs b/src/Markdig/Helpers/BlockWrapper.cs index 43cd9914e..38fe8992f 100644 --- a/src/Markdig/Helpers/BlockWrapper.cs +++ b/src/Markdig/Helpers/BlockWrapper.cs @@ -3,28 +3,26 @@ // See the license.txt file in the project root for more information. using Markdig.Syntax; -using System; -namespace Markdig.Helpers +namespace Markdig.Helpers; + +// Used to avoid the overhead of type covariance checks +internal readonly struct BlockWrapper : IEquatable { - // Used to avoid the overhead of type covariance checks - internal readonly struct BlockWrapper : IEquatable - { - public readonly Block Block; + public readonly Block Block; - public BlockWrapper(Block block) - { - Block = block; - } + public BlockWrapper(Block block) + { + Block = block; + } - public static implicit operator Block(BlockWrapper wrapper) => wrapper.Block; + public static implicit operator Block(BlockWrapper wrapper) => wrapper.Block; - public static implicit operator BlockWrapper(Block block) => new BlockWrapper(block); + public static implicit operator BlockWrapper(Block block) => new BlockWrapper(block); - public bool Equals(BlockWrapper other) => ReferenceEquals(Block, other.Block); + public bool Equals(BlockWrapper other) => ReferenceEquals(Block, other.Block); - public override bool Equals(object obj) => Block.Equals(obj); + public override bool Equals(object obj) => Block.Equals(obj); - public override int GetHashCode() => Block.GetHashCode(); - } + public override int GetHashCode() => Block.GetHashCode(); } diff --git a/src/Markdig/Helpers/CharHelper.cs b/src/Markdig/Helpers/CharHelper.cs index fab96848a..fcf27a6dc 100644 --- a/src/Markdig/Helpers/CharHelper.cs +++ b/src/Markdig/Helpers/CharHelper.cs @@ -2,750 +2,747 @@ // 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.Globalization; -using System.Collections.Generic; using System.Runtime.CompilerServices; -using System.Diagnostics; -namespace Markdig.Helpers +namespace Markdig.Helpers; + +/// +/// Helper class for handling characters. +/// +public static class CharHelper { - /// - /// Helper class for handling characters. - /// - public static class CharHelper - { - public const int TabSize = 4; + public const int TabSize = 4; - public const char ReplacementChar = '\uFFFD'; + public const char ReplacementChar = '\uFFFD'; - public const string ReplacementCharString = "\uFFFD"; + public const string ReplacementCharString = "\uFFFD"; - private const char HighSurrogateStart = '\ud800'; - private const char HighSurrogateEnd = '\udbff'; - private const char LowSurrogateStart = '\udc00'; - private const char LowSurrogateEnd = '\udfff'; + private const char HighSurrogateStart = '\ud800'; + private const char HighSurrogateEnd = '\udbff'; + private const char LowSurrogateStart = '\udc00'; + private const char LowSurrogateEnd = '\udfff'; - // We don't support LCDM - private static readonly Dictionary romanMap = new Dictionary(6) { - { 'i', 1 }, { 'v', 5 }, { 'x', 10 }, - { 'I', 1 }, { 'V', 5 }, { 'X', 10 } - }; + // We don't support LCDM + private static readonly Dictionary romanMap = new Dictionary(6) { + { 'i', 1 }, { 'v', 5 }, { 'x', 10 }, + { 'I', 1 }, { 'V', 5 }, { 'X', 10 } + }; - private static readonly char[] punctuationExceptions = { '−', '-', '†', '‡' }; + private static readonly char[] punctuationExceptions = { '−', '-', '†', '‡' }; - public static void CheckOpenCloseDelimiter(char pc, char c, bool enableWithinWord, out bool canOpen, out bool canClose) + public static void CheckOpenCloseDelimiter(char pc, char c, bool enableWithinWord, out bool canOpen, out bool canClose) + { + pc.CheckUnicodeCategory(out bool prevIsWhiteSpace, out bool prevIsPunctuation); + c.CheckUnicodeCategory(out bool nextIsWhiteSpace, out bool nextIsPunctuation); + + var prevIsExcepted = prevIsPunctuation && punctuationExceptions.Contains(pc); + var nextIsExcepted = nextIsPunctuation && punctuationExceptions.Contains(c); + + // A left-flanking delimiter run is a delimiter run that is + // (1) not followed by Unicode whitespace, and either + // (2a) not followed by a punctuation character or + // (2b) followed by a punctuation character and preceded by Unicode whitespace or a punctuation character. + // For purposes of this definition, the beginning and the end of the line count as Unicode whitespace. + canOpen = !nextIsWhiteSpace && + ((!nextIsPunctuation || nextIsExcepted) || prevIsWhiteSpace || prevIsPunctuation); + + + // A right-flanking delimiter run is a delimiter run that is + // (1) not preceded by Unicode whitespace, and either + // (2a) not preceded by a punctuation character, or + // (2b) preceded by a punctuation character and followed by Unicode whitespace or a punctuation character. + // For purposes of this definition, the beginning and the end of the line count as Unicode whitespace. + canClose = !prevIsWhiteSpace && + ((!prevIsPunctuation || prevIsExcepted) || nextIsWhiteSpace || nextIsPunctuation); + + if (!enableWithinWord) { - pc.CheckUnicodeCategory(out bool prevIsWhiteSpace, out bool prevIsPunctuation); - c.CheckUnicodeCategory(out bool nextIsWhiteSpace, out bool nextIsPunctuation); - - var prevIsExcepted = prevIsPunctuation && punctuationExceptions.Contains(pc); - var nextIsExcepted = nextIsPunctuation && punctuationExceptions.Contains(c); - - // A left-flanking delimiter run is a delimiter run that is - // (1) not followed by Unicode whitespace, and either - // (2a) not followed by a punctuation character or - // (2b) followed by a punctuation character and preceded by Unicode whitespace or a punctuation character. - // For purposes of this definition, the beginning and the end of the line count as Unicode whitespace. - canOpen = !nextIsWhiteSpace && - ((!nextIsPunctuation || nextIsExcepted) || prevIsWhiteSpace || prevIsPunctuation); - - - // A right-flanking delimiter run is a delimiter run that is - // (1) not preceded by Unicode whitespace, and either - // (2a) not preceded by a punctuation character, or - // (2b) preceded by a punctuation character and followed by Unicode whitespace or a punctuation character. - // For purposes of this definition, the beginning and the end of the line count as Unicode whitespace. - canClose = !prevIsWhiteSpace && - ((!prevIsPunctuation || prevIsExcepted) || nextIsWhiteSpace || nextIsPunctuation); - - if (!enableWithinWord) - { - var temp = canOpen; - // A single _ character can open emphasis iff it is part of a left-flanking delimiter run and either - // (a) not part of a right-flanking delimiter run or - // (b) part of a right-flanking delimiter run preceded by punctuation. - canOpen = canOpen && (!canClose || prevIsPunctuation); - - // A single _ character can close emphasis iff it is part of a right-flanking delimiter run and either - // (a) not part of a left-flanking delimiter run or - // (b) part of a left-flanking delimiter run followed by punctuation. - canClose = canClose && (!temp || nextIsPunctuation); - } + var temp = canOpen; + // A single _ character can open emphasis iff it is part of a left-flanking delimiter run and either + // (a) not part of a right-flanking delimiter run or + // (b) part of a right-flanking delimiter run preceded by punctuation. + canOpen = canOpen && (!canClose || prevIsPunctuation); + + // A single _ character can close emphasis iff it is part of a right-flanking delimiter run and either + // (a) not part of a left-flanking delimiter run or + // (b) part of a left-flanking delimiter run followed by punctuation. + canClose = canClose && (!temp || nextIsPunctuation); } + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsRomanLetterPartial(char c) - { - // We don't support LCDM - return IsRomanLetterLowerPartial(c) || IsRomanLetterUpperPartial(c); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsRomanLetterPartial(char c) + { + // We don't support LCDM + return IsRomanLetterLowerPartial(c) || IsRomanLetterUpperPartial(c); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsRomanLetterLowerPartial(char c) - { - // We don't support LCDM - return c == 'i' || c == 'v' || c == 'x'; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsRomanLetterLowerPartial(char c) + { + // We don't support LCDM + return c == 'i' || c == 'v' || c == 'x'; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsRomanLetterUpperPartial(char c) - { - // We don't support LCDM - return c == 'I' || c == 'V' || c == 'X'; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsRomanLetterUpperPartial(char c) + { + // We don't support LCDM + return c == 'I' || c == 'V' || c == 'X'; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int RomanToArabic(ReadOnlySpan text) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int RomanToArabic(ReadOnlySpan text) + { + int result = 0; + for (int i = 0; i < text.Length; i++) { - int result = 0; - for (int i = 0; i < text.Length; i++) + var candidate = romanMap[text[i]]; + if ((uint)(i + 1) < text.Length && candidate < romanMap[text[i + 1]]) { - var candidate = romanMap[text[i]]; - if ((uint)(i + 1) < text.Length && candidate < romanMap[text[i + 1]]) - { - result -= candidate; - } - else - { - result += candidate; - } + result -= candidate; } - return result; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int AddTab(int column) - { - // return ((column + TabSize) / TabSize) * TabSize; - Debug.Assert(TabSize == 4, "Change the AddTab implementation if TabSize is no longer a power of 2"); - return TabSize + (column & ~(TabSize - 1)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsAcrossTab(int column) - { - return (column & (TabSize - 1)) != 0; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool Contains(this char[] charList, char c) - { - foreach (char ch in charList) + else { - if (ch == c) - { - return true; - } + result += candidate; } - return false; } + return result; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsWhitespace(this char c) - { - // 2.1 Characters and lines - // A Unicode whitespace character is any code point in the Unicode Zs general category, - // or a tab (U+0009), line feed (U+000A), form feed (U+000C), or carriage return (U+000D). - if (c <= ' ') - { - const long Mask = - (1L << ' ') | - (1L << '\t') | - (1L << '\n') | - (1L << '\f') | - (1L << '\r'); - - return (Mask & (1L << c)) != 0; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int AddTab(int column) + { + // return ((column + TabSize) / TabSize) * TabSize; + Debug.Assert(TabSize == 4, "Change the AddTab implementation if TabSize is no longer a power of 2"); + return TabSize + (column & ~(TabSize - 1)); + } - return c >= '\u00A0' && IsWhitespaceRare(c); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAcrossTab(int column) + { + return (column & (TabSize - 1)) != 0; + } - static bool IsWhitespaceRare(char c) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Contains(this char[] charList, char c) + { + foreach (char ch in charList) + { + if (ch == c) { - // return CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.SpaceSeparator; - - if (c < 5760) - { - return c == '\u00A0'; - } - else - { - return c <= 12288 && - (c == 5760 || IsInInclusiveRange(c, 8192, 8202) || c == 8239 || c == 8287 || c == 12288); - } + return true; } } + return false; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsControl(this char c) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsWhitespace(this char c) + { + // 2.1 Characters and lines + // A Unicode whitespace character is any code point in the Unicode Zs general category, + // or a tab (U+0009), line feed (U+000A), form feed (U+000C), or carriage return (U+000D). + if (c <= ' ') { - return c < ' ' || char.IsControl(c); + const long Mask = + (1L << ' ') | + (1L << '\t') | + (1L << '\n') | + (1L << '\f') | + (1L << '\r'); + + return (Mask & (1L << c)) != 0; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsEscapableSymbol(this char c) - { - // char.IsSymbol also works with Unicode symbols that cannot be escaped based on the specification. - return (c > ' ' && c < '0') || (c > '9' && c < 'A') || (c > 'Z' && c < 'a') || (c > 'z' && c < 127) || c == '•'; - } + return c >= '\u00A0' && IsWhitespaceRare(c); - //[MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsWhiteSpaceOrZero(this char c) + static bool IsWhitespaceRare(char c) { - return IsZero(c) || IsWhitespace(c); - } + // return CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.SpaceSeparator; - // Check if a char is a space or a punctuation - public static void CheckUnicodeCategory(this char c, out bool space, out bool punctuation) - { - if (IsWhitespace(c)) - { - space = true; - punctuation = false; - } - else if (c <= 127) + if (c < 5760) { - space = c == '\0'; - punctuation = c == '\0' || IsAsciiPunctuation(c); + return c == '\u00A0'; } else { - // A Unicode punctuation character is an ASCII punctuation character - // or anything in the general Unicode categories Pc, Pd, Pe, Pf, Pi, Po, or Ps. - space = false; - UnicodeCategory category = CharUnicodeInfo.GetUnicodeCategory(c); - punctuation = category == UnicodeCategory.ConnectorPunctuation - || category == UnicodeCategory.DashPunctuation - || category == UnicodeCategory.OpenPunctuation - || category == UnicodeCategory.ClosePunctuation - || category == UnicodeCategory.InitialQuotePunctuation - || category == UnicodeCategory.FinalQuotePunctuation - || category == UnicodeCategory.OtherPunctuation; + return c <= 12288 && + (c == 5760 || IsInInclusiveRange(c, 8192, 8202) || c == 8239 || c == 8287 || c == 12288); } } + } - // Same as CheckUnicodeCategory - internal static bool IsSpaceOrPunctuation(this char c) - { - if (IsWhitespace(c)) - { - return true; - } - else if (c <= 127) - { - return c == '\0' || IsAsciiPunctuation(c); - } - else - { - var category = CharUnicodeInfo.GetUnicodeCategory(c); - return category == UnicodeCategory.ConnectorPunctuation - || category == UnicodeCategory.DashPunctuation - || category == UnicodeCategory.OpenPunctuation - || category == UnicodeCategory.ClosePunctuation - || category == UnicodeCategory.InitialQuotePunctuation - || category == UnicodeCategory.FinalQuotePunctuation - || category == UnicodeCategory.OtherPunctuation; - } - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsControl(this char c) + { + return c < ' ' || char.IsControl(c); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsNewLineOrLineFeed(this char c) - { - return c == '\n' || c == '\r'; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsEscapableSymbol(this char c) + { + // char.IsSymbol also works with Unicode symbols that cannot be escaped based on the specification. + return (c > ' ' && c < '0') || (c > '9' && c < 'A') || (c > 'Z' && c < 'a') || (c > 'z' && c < 127) || c == '•'; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsZero(this char c) - { - return c == '\0'; - } + //[MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsWhiteSpaceOrZero(this char c) + { + return IsZero(c) || IsWhitespace(c); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsSpace(this char c) + // Check if a char is a space or a punctuation + public static void CheckUnicodeCategory(this char c, out bool space, out bool punctuation) + { + if (IsWhitespace(c)) { - // 2.1 Characters and lines - // A space is U+0020. - return c == ' '; + space = true; + punctuation = false; } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsTab(this char c) + else if (c <= 127) { - // 2.1 Characters and lines - // A space is U+0009. - return c == '\t'; + space = c == '\0'; + punctuation = c == '\0' || IsAsciiPunctuation(c); } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsSpaceOrTab(this char c) + else { - return IsSpace(c) || IsTab(c); + // A Unicode punctuation character is an ASCII punctuation character + // or anything in the general Unicode categories Pc, Pd, Pe, Pf, Pi, Po, or Ps. + space = false; + UnicodeCategory category = CharUnicodeInfo.GetUnicodeCategory(c); + punctuation = category == UnicodeCategory.ConnectorPunctuation + || category == UnicodeCategory.DashPunctuation + || category == UnicodeCategory.OpenPunctuation + || category == UnicodeCategory.ClosePunctuation + || category == UnicodeCategory.InitialQuotePunctuation + || category == UnicodeCategory.FinalQuotePunctuation + || category == UnicodeCategory.OtherPunctuation; } + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static char EscapeInsecure(this char c) + // Same as CheckUnicodeCategory + internal static bool IsSpaceOrPunctuation(this char c) + { + if (IsWhitespace(c)) { - // 2.3 Insecure characters - // For security reasons, the Unicode character U+0000 must be replaced with the REPLACEMENT CHARACTER (U+FFFD). - return c == '\0' ? '\ufffd' : c; + return true; } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsAlphaUpper(this char c) + else if (c <= 127) { - return (uint)(c - 'A') <= ('Z' - 'A'); + return c == '\0' || IsAsciiPunctuation(c); } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsAlpha(this char c) + else { - return (uint)((c - 'A') & ~0x20) <= ('Z' - 'A'); + var category = CharUnicodeInfo.GetUnicodeCategory(c); + return category == UnicodeCategory.ConnectorPunctuation + || category == UnicodeCategory.DashPunctuation + || category == UnicodeCategory.OpenPunctuation + || category == UnicodeCategory.ClosePunctuation + || category == UnicodeCategory.InitialQuotePunctuation + || category == UnicodeCategory.FinalQuotePunctuation + || category == UnicodeCategory.OtherPunctuation; } + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsAlphaNumeric(this char c) - { - return IsAlpha(c) || IsDigit(c); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsNewLineOrLineFeed(this char c) + { + return c == '\n' || c == '\r'; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsDigit(this char c) - { - return (uint)(c - '0') <= ('9' - '0'); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsZero(this char c) + { + return c == '\0'; + } - public static bool IsAsciiPunctuation(this char c) - { - // 2.1 Characters and lines - // An ASCII punctuation character is - // !, ", #, $, %, &, ', (, ), *, +, ,, -, ., / (U+0021–2F), - // :, ;, <, =, >, ?, @ (U+003A–0040), - // [, \, ], ^, _, ` (U+005B–0060), - // {, |, }, or ~ (U+007B–007E). - return c <= 127 && ( - IsInInclusiveRange(c, 33, 47) || - IsInInclusiveRange(c, 58, 64) || - IsInInclusiveRange(c, 91, 96) || - IsInInclusiveRange(c, 123, 126)); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsSpace(this char c) + { + // 2.1 Characters and lines + // A space is U+0020. + return c == ' '; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsEmailUsernameSpecialChar(char c) - { - return ".!#$%&'*+/=?^_`{|}~-+.~".IndexOf(c) >= 0; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsTab(this char c) + { + // 2.1 Characters and lines + // A space is U+0009. + return c == '\t'; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsHighSurrogate(char c) - { - return IsInInclusiveRange(c, HighSurrogateStart, HighSurrogateEnd); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsSpaceOrTab(this char c) + { + return IsSpace(c) || IsTab(c); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsLowSurrogate(char c) - { - return IsInInclusiveRange(c, LowSurrogateStart, LowSurrogateEnd); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static char EscapeInsecure(this char c) + { + // 2.3 Insecure characters + // For security reasons, the Unicode character U+0000 must be replaced with the REPLACEMENT CHARACTER (U+FFFD). + return c == '\0' ? '\ufffd' : c; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAlphaUpper(this char c) + { + return (uint)(c - 'A') <= ('Z' - 'A'); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsInInclusiveRange(char c, char min, char max) - => (uint)(c - min) <= (uint)(max - min); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAlpha(this char c) + { + return (uint)((c - 'A') & ~0x20) <= ('Z' - 'A'); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool IsInInclusiveRange(int value, uint min, uint max) - => ((uint)value - min) <= (max - min); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAlphaNumeric(this char c) + { + return IsAlpha(c) || IsDigit(c); + } - public static bool IsRightToLeft(int c) - { - // Generated from Table D.1 of RFC3454 - // http://www.ietf.org/rfc/rfc3454.txt - - // Probably should use a binary search approach - - return c >= 0x0005D0 && c <= 0x0005EA || - c >= 0x0005F0 && c <= 0x0005F4 || - c >= 0x000621 && c <= 0x00063A || - c >= 0x000640 && c <= 0x00064A || - c >= 0x00066D && c <= 0x00066F || - c >= 0x000671 && c <= 0x0006D5 || - c >= 0x0006E5 && c <= 0x0006E6 || - c >= 0x0006FA && c <= 0x0006FE || - c >= 0x000700 && c <= 0x00070D || - c >= 0x000712 && c <= 0x00072C || - c >= 0x000780 && c <= 0x0007A5 || - c >= 0x00FB1F && c <= 0x00FB28 || - c >= 0x00FB2A && c <= 0x00FB36 || - c >= 0x00FB38 && c <= 0x00FB3C || - c >= 0x00FB40 && c <= 0x00FB41 || - c >= 0x00FB43 && c <= 0x00FB44 || - c >= 0x00FB46 && c <= 0x00FBB1 || - c >= 0x00FBD3 && c <= 0x00FD3D || - c >= 0x00FD50 && c <= 0x00FD8F || - c >= 0x00FD92 && c <= 0x00FDC7 || - c >= 0x00FDF0 && c <= 0x00FDFC || - c >= 0x00FE70 && c <= 0x00FE74 || - c >= 0x00FE76 && c <= 0x00FEFC || - c == 0x0005BE || c == 0x0005C0 || - c == 0x0005C3 || c == 0x00061B || - c == 0x00061F || c == 0x0006DD || - c == 0x000710 || c == 0x0007B1 || - c == 0x00200F || c == 0x00FB1D || - c == 0x00FB3E; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsDigit(this char c) + { + return (uint)(c - '0') <= ('9' - '0'); + } - public static bool IsLeftToRight(int c) - { - // Generated from Table D.2 of RFC3454 - // http://www.ietf.org/rfc/rfc3454.txt - - // Probably should use a binary search approach - - return c >= 0x000041 && c <= 0x00005A || - c >= 0x000061 && c <= 0x00007A || - c >= 0x0000C0 && c <= 0x0000D6 || - c >= 0x0000D8 && c <= 0x0000F6 || - c >= 0x0000F8 && c <= 0x000220 || - c >= 0x000222 && c <= 0x000233 || - c >= 0x000250 && c <= 0x0002AD || - c >= 0x0002B0 && c <= 0x0002B8 || - c >= 0x0002BB && c <= 0x0002C1 || - c >= 0x0002D0 && c <= 0x0002D1 || - c >= 0x0002E0 && c <= 0x0002E4 || - c >= 0x000388 && c <= 0x00038A || - c >= 0x00038E && c <= 0x0003A1 || - c >= 0x0003A3 && c <= 0x0003CE || - c >= 0x0003D0 && c <= 0x0003F5 || - c >= 0x000400 && c <= 0x000482 || - c >= 0x00048A && c <= 0x0004CE || - c >= 0x0004D0 && c <= 0x0004F5 || - c >= 0x0004F8 && c <= 0x0004F9 || - c >= 0x000500 && c <= 0x00050F || - c >= 0x000531 && c <= 0x000556 || - c >= 0x000559 && c <= 0x00055F || - c >= 0x000561 && c <= 0x000587 || - c >= 0x000905 && c <= 0x000939 || - c >= 0x00093D && c <= 0x000940 || - c >= 0x000949 && c <= 0x00094C || - c >= 0x000958 && c <= 0x000961 || - c >= 0x000964 && c <= 0x000970 || - c >= 0x000982 && c <= 0x000983 || - c >= 0x000985 && c <= 0x00098C || - c >= 0x00098F && c <= 0x000990 || - c >= 0x000993 && c <= 0x0009A8 || - c >= 0x0009AA && c <= 0x0009B0 || - c >= 0x0009B6 && c <= 0x0009B9 || - c >= 0x0009BE && c <= 0x0009C0 || - c >= 0x0009C7 && c <= 0x0009C8 || - c >= 0x0009CB && c <= 0x0009CC || - c >= 0x0009DC && c <= 0x0009DD || - c >= 0x0009DF && c <= 0x0009E1 || - c >= 0x0009E6 && c <= 0x0009F1 || - c >= 0x0009F4 && c <= 0x0009FA || - c >= 0x000A05 && c <= 0x000A0A || - c >= 0x000A0F && c <= 0x000A10 || - c >= 0x000A13 && c <= 0x000A28 || - c >= 0x000A2A && c <= 0x000A30 || - c >= 0x000A32 && c <= 0x000A33 || - c >= 0x000A35 && c <= 0x000A36 || - c >= 0x000A38 && c <= 0x000A39 || - c >= 0x000A3E && c <= 0x000A40 || - c >= 0x000A59 && c <= 0x000A5C || - c >= 0x000A66 && c <= 0x000A6F || - c >= 0x000A72 && c <= 0x000A74 || - c >= 0x000A85 && c <= 0x000A8B || - c >= 0x000A8F && c <= 0x000A91 || - c >= 0x000A93 && c <= 0x000AA8 || - c >= 0x000AAA && c <= 0x000AB0 || - c >= 0x000AB2 && c <= 0x000AB3 || - c >= 0x000AB5 && c <= 0x000AB9 || - c >= 0x000ABD && c <= 0x000AC0 || - c >= 0x000ACB && c <= 0x000ACC || - c >= 0x000AE6 && c <= 0x000AEF || - c >= 0x000B02 && c <= 0x000B03 || - c >= 0x000B05 && c <= 0x000B0C || - c >= 0x000B0F && c <= 0x000B10 || - c >= 0x000B13 && c <= 0x000B28 || - c >= 0x000B2A && c <= 0x000B30 || - c >= 0x000B32 && c <= 0x000B33 || - c >= 0x000B36 && c <= 0x000B39 || - c >= 0x000B3D && c <= 0x000B3E || - c >= 0x000B47 && c <= 0x000B48 || - c >= 0x000B4B && c <= 0x000B4C || - c >= 0x000B5C && c <= 0x000B5D || - c >= 0x000B5F && c <= 0x000B61 || - c >= 0x000B66 && c <= 0x000B70 || - c >= 0x000B85 && c <= 0x000B8A || - c >= 0x000B8E && c <= 0x000B90 || - c >= 0x000B92 && c <= 0x000B95 || - c >= 0x000B99 && c <= 0x000B9A || - c >= 0x000B9E && c <= 0x000B9F || - c >= 0x000BA3 && c <= 0x000BA4 || - c >= 0x000BA8 && c <= 0x000BAA || - c >= 0x000BAE && c <= 0x000BB5 || - c >= 0x000BB7 && c <= 0x000BB9 || - c >= 0x000BBE && c <= 0x000BBF || - c >= 0x000BC1 && c <= 0x000BC2 || - c >= 0x000BC6 && c <= 0x000BC8 || - c >= 0x000BCA && c <= 0x000BCC || - c >= 0x000BE7 && c <= 0x000BF2 || - c >= 0x000C01 && c <= 0x000C03 || - c >= 0x000C05 && c <= 0x000C0C || - c >= 0x000C0E && c <= 0x000C10 || - c >= 0x000C12 && c <= 0x000C28 || - c >= 0x000C2A && c <= 0x000C33 || - c >= 0x000C35 && c <= 0x000C39 || - c >= 0x000C41 && c <= 0x000C44 || - c >= 0x000C60 && c <= 0x000C61 || - c >= 0x000C66 && c <= 0x000C6F || - c >= 0x000C82 && c <= 0x000C83 || - c >= 0x000C85 && c <= 0x000C8C || - c >= 0x000C8E && c <= 0x000C90 || - c >= 0x000C92 && c <= 0x000CA8 || - c >= 0x000CAA && c <= 0x000CB3 || - c >= 0x000CB5 && c <= 0x000CB9 || - c >= 0x000CC0 && c <= 0x000CC4 || - c >= 0x000CC7 && c <= 0x000CC8 || - c >= 0x000CCA && c <= 0x000CCB || - c >= 0x000CD5 && c <= 0x000CD6 || - c >= 0x000CE0 && c <= 0x000CE1 || - c >= 0x000CE6 && c <= 0x000CEF || - c >= 0x000D02 && c <= 0x000D03 || - c >= 0x000D05 && c <= 0x000D0C || - c >= 0x000D0E && c <= 0x000D10 || - c >= 0x000D12 && c <= 0x000D28 || - c >= 0x000D2A && c <= 0x000D39 || - c >= 0x000D3E && c <= 0x000D40 || - c >= 0x000D46 && c <= 0x000D48 || - c >= 0x000D4A && c <= 0x000D4C || - c >= 0x000D60 && c <= 0x000D61 || - c >= 0x000D66 && c <= 0x000D6F || - c >= 0x000D82 && c <= 0x000D83 || - c >= 0x000D85 && c <= 0x000D96 || - c >= 0x000D9A && c <= 0x000DB1 || - c >= 0x000DB3 && c <= 0x000DBB || - c >= 0x000DC0 && c <= 0x000DC6 || - c >= 0x000DCF && c <= 0x000DD1 || - c >= 0x000DD8 && c <= 0x000DDF || - c >= 0x000DF2 && c <= 0x000DF4 || - c >= 0x000E01 && c <= 0x000E30 || - c >= 0x000E32 && c <= 0x000E33 || - c >= 0x000E40 && c <= 0x000E46 || - c >= 0x000E4F && c <= 0x000E5B || - c >= 0x000E81 && c <= 0x000E82 || - c >= 0x000E87 && c <= 0x000E88 || - c >= 0x000E94 && c <= 0x000E97 || - c >= 0x000E99 && c <= 0x000E9F || - c >= 0x000EA1 && c <= 0x000EA3 || - c >= 0x000EAA && c <= 0x000EAB || - c >= 0x000EAD && c <= 0x000EB0 || - c >= 0x000EB2 && c <= 0x000EB3 || - c >= 0x000EC0 && c <= 0x000EC4 || - c >= 0x000ED0 && c <= 0x000ED9 || - c >= 0x000EDC && c <= 0x000EDD || - c >= 0x000F00 && c <= 0x000F17 || - c >= 0x000F1A && c <= 0x000F34 || - c >= 0x000F3E && c <= 0x000F47 || - c >= 0x000F49 && c <= 0x000F6A || - c >= 0x000F88 && c <= 0x000F8B || - c >= 0x000FBE && c <= 0x000FC5 || - c >= 0x000FC7 && c <= 0x000FCC || - c >= 0x001000 && c <= 0x001021 || - c >= 0x001023 && c <= 0x001027 || - c >= 0x001029 && c <= 0x00102A || - c >= 0x001040 && c <= 0x001057 || - c >= 0x0010A0 && c <= 0x0010C5 || - c >= 0x0010D0 && c <= 0x0010F8 || - c >= 0x001100 && c <= 0x001159 || - c >= 0x00115F && c <= 0x0011A2 || - c >= 0x0011A8 && c <= 0x0011F9 || - c >= 0x001200 && c <= 0x001206 || - c >= 0x001208 && c <= 0x001246 || - c >= 0x00124A && c <= 0x00124D || - c >= 0x001250 && c <= 0x001256 || - c >= 0x00125A && c <= 0x00125D || - c >= 0x001260 && c <= 0x001286 || - c >= 0x00128A && c <= 0x00128D || - c >= 0x001290 && c <= 0x0012AE || - c >= 0x0012B2 && c <= 0x0012B5 || - c >= 0x0012B8 && c <= 0x0012BE || - c >= 0x0012C2 && c <= 0x0012C5 || - c >= 0x0012C8 && c <= 0x0012CE || - c >= 0x0012D0 && c <= 0x0012D6 || - c >= 0x0012D8 && c <= 0x0012EE || - c >= 0x0012F0 && c <= 0x00130E || - c >= 0x001312 && c <= 0x001315 || - c >= 0x001318 && c <= 0x00131E || - c >= 0x001320 && c <= 0x001346 || - c >= 0x001348 && c <= 0x00135A || - c >= 0x001361 && c <= 0x00137C || - c >= 0x0013A0 && c <= 0x0013F4 || - c >= 0x001401 && c <= 0x001676 || - c >= 0x001681 && c <= 0x00169A || - c >= 0x0016A0 && c <= 0x0016F0 || - c >= 0x001700 && c <= 0x00170C || - c >= 0x00170E && c <= 0x001711 || - c >= 0x001720 && c <= 0x001731 || - c >= 0x001735 && c <= 0x001736 || - c >= 0x001740 && c <= 0x001751 || - c >= 0x001760 && c <= 0x00176C || - c >= 0x00176E && c <= 0x001770 || - c >= 0x001780 && c <= 0x0017B6 || - c >= 0x0017BE && c <= 0x0017C5 || - c >= 0x0017C7 && c <= 0x0017C8 || - c >= 0x0017D4 && c <= 0x0017DA || - c >= 0x0017E0 && c <= 0x0017E9 || - c >= 0x001810 && c <= 0x001819 || - c >= 0x001820 && c <= 0x001877 || - c >= 0x001880 && c <= 0x0018A8 || - c >= 0x001E00 && c <= 0x001E9B || - c >= 0x001EA0 && c <= 0x001EF9 || - c >= 0x001F00 && c <= 0x001F15 || - c >= 0x001F18 && c <= 0x001F1D || - c >= 0x001F20 && c <= 0x001F45 || - c >= 0x001F48 && c <= 0x001F4D || - c >= 0x001F50 && c <= 0x001F57 || - c >= 0x001F5F && c <= 0x001F7D || - c >= 0x001F80 && c <= 0x001FB4 || - c >= 0x001FB6 && c <= 0x001FBC || - c >= 0x001FC2 && c <= 0x001FC4 || - c >= 0x001FC6 && c <= 0x001FCC || - c >= 0x001FD0 && c <= 0x001FD3 || - c >= 0x001FD6 && c <= 0x001FDB || - c >= 0x001FE0 && c <= 0x001FEC || - c >= 0x001FF2 && c <= 0x001FF4 || - c >= 0x001FF6 && c <= 0x001FFC || - c >= 0x00210A && c <= 0x002113 || - c >= 0x002119 && c <= 0x00211D || - c >= 0x00212A && c <= 0x00212D || - c >= 0x00212F && c <= 0x002131 || - c >= 0x002133 && c <= 0x002139 || - c >= 0x00213D && c <= 0x00213F || - c >= 0x002145 && c <= 0x002149 || - c >= 0x002160 && c <= 0x002183 || - c >= 0x002336 && c <= 0x00237A || - c >= 0x00249C && c <= 0x0024E9 || - c >= 0x003005 && c <= 0x003007 || - c >= 0x003021 && c <= 0x003029 || - c >= 0x003031 && c <= 0x003035 || - c >= 0x003038 && c <= 0x00303C || - c >= 0x003041 && c <= 0x003096 || - c >= 0x00309D && c <= 0x00309F || - c >= 0x0030A1 && c <= 0x0030FA || - c >= 0x0030FC && c <= 0x0030FF || - c >= 0x003105 && c <= 0x00312C || - c >= 0x003131 && c <= 0x00318E || - c >= 0x003190 && c <= 0x0031B7 || - c >= 0x0031F0 && c <= 0x00321C || - c >= 0x003220 && c <= 0x003243 || - c >= 0x003260 && c <= 0x00327B || - c >= 0x00327F && c <= 0x0032B0 || - c >= 0x0032C0 && c <= 0x0032CB || - c >= 0x0032D0 && c <= 0x0032FE || - c >= 0x003300 && c <= 0x003376 || - c >= 0x00337B && c <= 0x0033DD || - c >= 0x0033E0 && c <= 0x0033FE || - c >= 0x003400 && c <= 0x004DB5 || - c >= 0x004E00 && c <= 0x009FA5 || - c >= 0x00A000 && c <= 0x00A48C || - c >= 0x00AC00 && c <= 0x00D7A3 || - c >= 0x00D800 && c <= 0x00FA2D || - c >= 0x00FA30 && c <= 0x00FA6A || - c >= 0x00FB00 && c <= 0x00FB06 || - c >= 0x00FB13 && c <= 0x00FB17 || - c >= 0x00FF21 && c <= 0x00FF3A || - c >= 0x00FF41 && c <= 0x00FF5A || - c >= 0x00FF66 && c <= 0x00FFBE || - c >= 0x00FFC2 && c <= 0x00FFC7 || - c >= 0x00FFCA && c <= 0x00FFCF || - c >= 0x00FFD2 && c <= 0x00FFD7 || - c >= 0x00FFDA && c <= 0x00FFDC || - c >= 0x010300 && c <= 0x01031E || - c >= 0x010320 && c <= 0x010323 || - c >= 0x010330 && c <= 0x01034A || - c >= 0x010400 && c <= 0x010425 || - c >= 0x010428 && c <= 0x01044D || - c >= 0x01D000 && c <= 0x01D0F5 || - c >= 0x01D100 && c <= 0x01D126 || - c >= 0x01D12A && c <= 0x01D166 || - c >= 0x01D16A && c <= 0x01D172 || - c >= 0x01D183 && c <= 0x01D184 || - c >= 0x01D18C && c <= 0x01D1A9 || - c >= 0x01D1AE && c <= 0x01D1DD || - c >= 0x01D400 && c <= 0x01D454 || - c >= 0x01D456 && c <= 0x01D49C || - c >= 0x01D49E && c <= 0x01D49F || - c >= 0x01D4A5 && c <= 0x01D4A6 || - c >= 0x01D4A9 && c <= 0x01D4AC || - c >= 0x01D4AE && c <= 0x01D4B9 || - c >= 0x01D4BD && c <= 0x01D4C0 || - c >= 0x01D4C2 && c <= 0x01D4C3 || - c >= 0x01D4C5 && c <= 0x01D505 || - c >= 0x01D507 && c <= 0x01D50A || - c >= 0x01D50D && c <= 0x01D514 || - c >= 0x01D516 && c <= 0x01D51C || - c >= 0x01D51E && c <= 0x01D539 || - c >= 0x01D53B && c <= 0x01D53E || - c >= 0x01D540 && c <= 0x01D544 || - c >= 0x01D54A && c <= 0x01D550 || - c >= 0x01D552 && c <= 0x01D6A3 || - c >= 0x01D6A8 && c <= 0x01D7C9 || - c >= 0x020000 && c <= 0x02A6D6 || - c >= 0x02F800 && c <= 0x02FA1D || - c >= 0x0F0000 && c <= 0x0FFFFD || - c >= 0x100000 && c <= 0x10FFFD || - c == 0x0000AA || c == 0x0000B5 || - c == 0x0000BA || c == 0x0002EE || - c == 0x00037A || c == 0x000386 || - c == 0x00038C || c == 0x000589 || - c == 0x000903 || c == 0x000950 || - c == 0x0009B2 || c == 0x0009D7 || - c == 0x000A5E || c == 0x000A83 || - c == 0x000A8D || c == 0x000AC9 || - c == 0x000AD0 || c == 0x000AE0 || - c == 0x000B40 || c == 0x000B57 || - c == 0x000B83 || c == 0x000B9C || - c == 0x000BD7 || c == 0x000CBE || - c == 0x000CDE || c == 0x000D57 || - c == 0x000DBD || c == 0x000E84 || - c == 0x000E8A || c == 0x000E8D || - c == 0x000EA5 || c == 0x000EA7 || - c == 0x000EBD || c == 0x000EC6 || - c == 0x000F36 || c == 0x000F38 || - c == 0x000F7F || c == 0x000F85 || - c == 0x000FCF || c == 0x00102C || - c == 0x001031 || c == 0x001038 || - c == 0x0010FB || c == 0x001248 || - c == 0x001258 || c == 0x001288 || - c == 0x0012B0 || c == 0x0012C0 || - c == 0x001310 || c == 0x0017DC || - c == 0x001F59 || c == 0x001F5B || - c == 0x001F5D || c == 0x001FBE || - c == 0x00200E || c == 0x002071 || - c == 0x00207F || c == 0x002102 || - c == 0x002107 || c == 0x002115 || - c == 0x002124 || c == 0x002126 || - c == 0x002128 || c == 0x002395 || - c == 0x01D4A2 || c == 0x01D4BB || - c == 0x01D546; - } + public static bool IsAsciiPunctuation(this char c) + { + // 2.1 Characters and lines + // An ASCII punctuation character is + // !, ", #, $, %, &, ', (, ), *, +, ,, -, ., / (U+0021–2F), + // :, ;, <, =, >, ?, @ (U+003A–0040), + // [, \, ], ^, _, ` (U+005B–0060), + // {, |, }, or ~ (U+007B–007E). + return c <= 127 && ( + IsInInclusiveRange(c, 33, 47) || + IsInInclusiveRange(c, 58, 64) || + IsInInclusiveRange(c, 91, 96) || + IsInInclusiveRange(c, 123, 126)); + } - // Used by ListExtraItemParser to format numbers from 1 - 26 - private static readonly string[] smallNumberStringCache = { - "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", - "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", - "20", "21", "22", "23", "24", "25", "26", - }; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsEmailUsernameSpecialChar(char c) + { + return ".!#$%&'*+/=?^_`{|}~-+.~".IndexOf(c) >= 0; + } - internal static string SmallNumberToString(int number) - { - string[] cache = smallNumberStringCache; - if ((uint)number < (uint)cache.Length) - { - return cache[number]; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsHighSurrogate(char c) + { + return IsInInclusiveRange(c, HighSurrogateStart, HighSurrogateEnd); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsLowSurrogate(char c) + { + return IsInInclusiveRange(c, LowSurrogateStart, LowSurrogateEnd); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsInInclusiveRange(char c, char min, char max) + => (uint)(c - min) <= (uint)(max - min); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsInInclusiveRange(int value, uint min, uint max) + => ((uint)value - min) <= (max - min); + + public static bool IsRightToLeft(int c) + { + // Generated from Table D.1 of RFC3454 + // http://www.ietf.org/rfc/rfc3454.txt + + // Probably should use a binary search approach + + return c >= 0x0005D0 && c <= 0x0005EA || + c >= 0x0005F0 && c <= 0x0005F4 || + c >= 0x000621 && c <= 0x00063A || + c >= 0x000640 && c <= 0x00064A || + c >= 0x00066D && c <= 0x00066F || + c >= 0x000671 && c <= 0x0006D5 || + c >= 0x0006E5 && c <= 0x0006E6 || + c >= 0x0006FA && c <= 0x0006FE || + c >= 0x000700 && c <= 0x00070D || + c >= 0x000712 && c <= 0x00072C || + c >= 0x000780 && c <= 0x0007A5 || + c >= 0x00FB1F && c <= 0x00FB28 || + c >= 0x00FB2A && c <= 0x00FB36 || + c >= 0x00FB38 && c <= 0x00FB3C || + c >= 0x00FB40 && c <= 0x00FB41 || + c >= 0x00FB43 && c <= 0x00FB44 || + c >= 0x00FB46 && c <= 0x00FBB1 || + c >= 0x00FBD3 && c <= 0x00FD3D || + c >= 0x00FD50 && c <= 0x00FD8F || + c >= 0x00FD92 && c <= 0x00FDC7 || + c >= 0x00FDF0 && c <= 0x00FDFC || + c >= 0x00FE70 && c <= 0x00FE74 || + c >= 0x00FE76 && c <= 0x00FEFC || + c == 0x0005BE || c == 0x0005C0 || + c == 0x0005C3 || c == 0x00061B || + c == 0x00061F || c == 0x0006DD || + c == 0x000710 || c == 0x0007B1 || + c == 0x00200F || c == 0x00FB1D || + c == 0x00FB3E; + } + + public static bool IsLeftToRight(int c) + { + // Generated from Table D.2 of RFC3454 + // http://www.ietf.org/rfc/rfc3454.txt + + // Probably should use a binary search approach + + return c >= 0x000041 && c <= 0x00005A || + c >= 0x000061 && c <= 0x00007A || + c >= 0x0000C0 && c <= 0x0000D6 || + c >= 0x0000D8 && c <= 0x0000F6 || + c >= 0x0000F8 && c <= 0x000220 || + c >= 0x000222 && c <= 0x000233 || + c >= 0x000250 && c <= 0x0002AD || + c >= 0x0002B0 && c <= 0x0002B8 || + c >= 0x0002BB && c <= 0x0002C1 || + c >= 0x0002D0 && c <= 0x0002D1 || + c >= 0x0002E0 && c <= 0x0002E4 || + c >= 0x000388 && c <= 0x00038A || + c >= 0x00038E && c <= 0x0003A1 || + c >= 0x0003A3 && c <= 0x0003CE || + c >= 0x0003D0 && c <= 0x0003F5 || + c >= 0x000400 && c <= 0x000482 || + c >= 0x00048A && c <= 0x0004CE || + c >= 0x0004D0 && c <= 0x0004F5 || + c >= 0x0004F8 && c <= 0x0004F9 || + c >= 0x000500 && c <= 0x00050F || + c >= 0x000531 && c <= 0x000556 || + c >= 0x000559 && c <= 0x00055F || + c >= 0x000561 && c <= 0x000587 || + c >= 0x000905 && c <= 0x000939 || + c >= 0x00093D && c <= 0x000940 || + c >= 0x000949 && c <= 0x00094C || + c >= 0x000958 && c <= 0x000961 || + c >= 0x000964 && c <= 0x000970 || + c >= 0x000982 && c <= 0x000983 || + c >= 0x000985 && c <= 0x00098C || + c >= 0x00098F && c <= 0x000990 || + c >= 0x000993 && c <= 0x0009A8 || + c >= 0x0009AA && c <= 0x0009B0 || + c >= 0x0009B6 && c <= 0x0009B9 || + c >= 0x0009BE && c <= 0x0009C0 || + c >= 0x0009C7 && c <= 0x0009C8 || + c >= 0x0009CB && c <= 0x0009CC || + c >= 0x0009DC && c <= 0x0009DD || + c >= 0x0009DF && c <= 0x0009E1 || + c >= 0x0009E6 && c <= 0x0009F1 || + c >= 0x0009F4 && c <= 0x0009FA || + c >= 0x000A05 && c <= 0x000A0A || + c >= 0x000A0F && c <= 0x000A10 || + c >= 0x000A13 && c <= 0x000A28 || + c >= 0x000A2A && c <= 0x000A30 || + c >= 0x000A32 && c <= 0x000A33 || + c >= 0x000A35 && c <= 0x000A36 || + c >= 0x000A38 && c <= 0x000A39 || + c >= 0x000A3E && c <= 0x000A40 || + c >= 0x000A59 && c <= 0x000A5C || + c >= 0x000A66 && c <= 0x000A6F || + c >= 0x000A72 && c <= 0x000A74 || + c >= 0x000A85 && c <= 0x000A8B || + c >= 0x000A8F && c <= 0x000A91 || + c >= 0x000A93 && c <= 0x000AA8 || + c >= 0x000AAA && c <= 0x000AB0 || + c >= 0x000AB2 && c <= 0x000AB3 || + c >= 0x000AB5 && c <= 0x000AB9 || + c >= 0x000ABD && c <= 0x000AC0 || + c >= 0x000ACB && c <= 0x000ACC || + c >= 0x000AE6 && c <= 0x000AEF || + c >= 0x000B02 && c <= 0x000B03 || + c >= 0x000B05 && c <= 0x000B0C || + c >= 0x000B0F && c <= 0x000B10 || + c >= 0x000B13 && c <= 0x000B28 || + c >= 0x000B2A && c <= 0x000B30 || + c >= 0x000B32 && c <= 0x000B33 || + c >= 0x000B36 && c <= 0x000B39 || + c >= 0x000B3D && c <= 0x000B3E || + c >= 0x000B47 && c <= 0x000B48 || + c >= 0x000B4B && c <= 0x000B4C || + c >= 0x000B5C && c <= 0x000B5D || + c >= 0x000B5F && c <= 0x000B61 || + c >= 0x000B66 && c <= 0x000B70 || + c >= 0x000B85 && c <= 0x000B8A || + c >= 0x000B8E && c <= 0x000B90 || + c >= 0x000B92 && c <= 0x000B95 || + c >= 0x000B99 && c <= 0x000B9A || + c >= 0x000B9E && c <= 0x000B9F || + c >= 0x000BA3 && c <= 0x000BA4 || + c >= 0x000BA8 && c <= 0x000BAA || + c >= 0x000BAE && c <= 0x000BB5 || + c >= 0x000BB7 && c <= 0x000BB9 || + c >= 0x000BBE && c <= 0x000BBF || + c >= 0x000BC1 && c <= 0x000BC2 || + c >= 0x000BC6 && c <= 0x000BC8 || + c >= 0x000BCA && c <= 0x000BCC || + c >= 0x000BE7 && c <= 0x000BF2 || + c >= 0x000C01 && c <= 0x000C03 || + c >= 0x000C05 && c <= 0x000C0C || + c >= 0x000C0E && c <= 0x000C10 || + c >= 0x000C12 && c <= 0x000C28 || + c >= 0x000C2A && c <= 0x000C33 || + c >= 0x000C35 && c <= 0x000C39 || + c >= 0x000C41 && c <= 0x000C44 || + c >= 0x000C60 && c <= 0x000C61 || + c >= 0x000C66 && c <= 0x000C6F || + c >= 0x000C82 && c <= 0x000C83 || + c >= 0x000C85 && c <= 0x000C8C || + c >= 0x000C8E && c <= 0x000C90 || + c >= 0x000C92 && c <= 0x000CA8 || + c >= 0x000CAA && c <= 0x000CB3 || + c >= 0x000CB5 && c <= 0x000CB9 || + c >= 0x000CC0 && c <= 0x000CC4 || + c >= 0x000CC7 && c <= 0x000CC8 || + c >= 0x000CCA && c <= 0x000CCB || + c >= 0x000CD5 && c <= 0x000CD6 || + c >= 0x000CE0 && c <= 0x000CE1 || + c >= 0x000CE6 && c <= 0x000CEF || + c >= 0x000D02 && c <= 0x000D03 || + c >= 0x000D05 && c <= 0x000D0C || + c >= 0x000D0E && c <= 0x000D10 || + c >= 0x000D12 && c <= 0x000D28 || + c >= 0x000D2A && c <= 0x000D39 || + c >= 0x000D3E && c <= 0x000D40 || + c >= 0x000D46 && c <= 0x000D48 || + c >= 0x000D4A && c <= 0x000D4C || + c >= 0x000D60 && c <= 0x000D61 || + c >= 0x000D66 && c <= 0x000D6F || + c >= 0x000D82 && c <= 0x000D83 || + c >= 0x000D85 && c <= 0x000D96 || + c >= 0x000D9A && c <= 0x000DB1 || + c >= 0x000DB3 && c <= 0x000DBB || + c >= 0x000DC0 && c <= 0x000DC6 || + c >= 0x000DCF && c <= 0x000DD1 || + c >= 0x000DD8 && c <= 0x000DDF || + c >= 0x000DF2 && c <= 0x000DF4 || + c >= 0x000E01 && c <= 0x000E30 || + c >= 0x000E32 && c <= 0x000E33 || + c >= 0x000E40 && c <= 0x000E46 || + c >= 0x000E4F && c <= 0x000E5B || + c >= 0x000E81 && c <= 0x000E82 || + c >= 0x000E87 && c <= 0x000E88 || + c >= 0x000E94 && c <= 0x000E97 || + c >= 0x000E99 && c <= 0x000E9F || + c >= 0x000EA1 && c <= 0x000EA3 || + c >= 0x000EAA && c <= 0x000EAB || + c >= 0x000EAD && c <= 0x000EB0 || + c >= 0x000EB2 && c <= 0x000EB3 || + c >= 0x000EC0 && c <= 0x000EC4 || + c >= 0x000ED0 && c <= 0x000ED9 || + c >= 0x000EDC && c <= 0x000EDD || + c >= 0x000F00 && c <= 0x000F17 || + c >= 0x000F1A && c <= 0x000F34 || + c >= 0x000F3E && c <= 0x000F47 || + c >= 0x000F49 && c <= 0x000F6A || + c >= 0x000F88 && c <= 0x000F8B || + c >= 0x000FBE && c <= 0x000FC5 || + c >= 0x000FC7 && c <= 0x000FCC || + c >= 0x001000 && c <= 0x001021 || + c >= 0x001023 && c <= 0x001027 || + c >= 0x001029 && c <= 0x00102A || + c >= 0x001040 && c <= 0x001057 || + c >= 0x0010A0 && c <= 0x0010C5 || + c >= 0x0010D0 && c <= 0x0010F8 || + c >= 0x001100 && c <= 0x001159 || + c >= 0x00115F && c <= 0x0011A2 || + c >= 0x0011A8 && c <= 0x0011F9 || + c >= 0x001200 && c <= 0x001206 || + c >= 0x001208 && c <= 0x001246 || + c >= 0x00124A && c <= 0x00124D || + c >= 0x001250 && c <= 0x001256 || + c >= 0x00125A && c <= 0x00125D || + c >= 0x001260 && c <= 0x001286 || + c >= 0x00128A && c <= 0x00128D || + c >= 0x001290 && c <= 0x0012AE || + c >= 0x0012B2 && c <= 0x0012B5 || + c >= 0x0012B8 && c <= 0x0012BE || + c >= 0x0012C2 && c <= 0x0012C5 || + c >= 0x0012C8 && c <= 0x0012CE || + c >= 0x0012D0 && c <= 0x0012D6 || + c >= 0x0012D8 && c <= 0x0012EE || + c >= 0x0012F0 && c <= 0x00130E || + c >= 0x001312 && c <= 0x001315 || + c >= 0x001318 && c <= 0x00131E || + c >= 0x001320 && c <= 0x001346 || + c >= 0x001348 && c <= 0x00135A || + c >= 0x001361 && c <= 0x00137C || + c >= 0x0013A0 && c <= 0x0013F4 || + c >= 0x001401 && c <= 0x001676 || + c >= 0x001681 && c <= 0x00169A || + c >= 0x0016A0 && c <= 0x0016F0 || + c >= 0x001700 && c <= 0x00170C || + c >= 0x00170E && c <= 0x001711 || + c >= 0x001720 && c <= 0x001731 || + c >= 0x001735 && c <= 0x001736 || + c >= 0x001740 && c <= 0x001751 || + c >= 0x001760 && c <= 0x00176C || + c >= 0x00176E && c <= 0x001770 || + c >= 0x001780 && c <= 0x0017B6 || + c >= 0x0017BE && c <= 0x0017C5 || + c >= 0x0017C7 && c <= 0x0017C8 || + c >= 0x0017D4 && c <= 0x0017DA || + c >= 0x0017E0 && c <= 0x0017E9 || + c >= 0x001810 && c <= 0x001819 || + c >= 0x001820 && c <= 0x001877 || + c >= 0x001880 && c <= 0x0018A8 || + c >= 0x001E00 && c <= 0x001E9B || + c >= 0x001EA0 && c <= 0x001EF9 || + c >= 0x001F00 && c <= 0x001F15 || + c >= 0x001F18 && c <= 0x001F1D || + c >= 0x001F20 && c <= 0x001F45 || + c >= 0x001F48 && c <= 0x001F4D || + c >= 0x001F50 && c <= 0x001F57 || + c >= 0x001F5F && c <= 0x001F7D || + c >= 0x001F80 && c <= 0x001FB4 || + c >= 0x001FB6 && c <= 0x001FBC || + c >= 0x001FC2 && c <= 0x001FC4 || + c >= 0x001FC6 && c <= 0x001FCC || + c >= 0x001FD0 && c <= 0x001FD3 || + c >= 0x001FD6 && c <= 0x001FDB || + c >= 0x001FE0 && c <= 0x001FEC || + c >= 0x001FF2 && c <= 0x001FF4 || + c >= 0x001FF6 && c <= 0x001FFC || + c >= 0x00210A && c <= 0x002113 || + c >= 0x002119 && c <= 0x00211D || + c >= 0x00212A && c <= 0x00212D || + c >= 0x00212F && c <= 0x002131 || + c >= 0x002133 && c <= 0x002139 || + c >= 0x00213D && c <= 0x00213F || + c >= 0x002145 && c <= 0x002149 || + c >= 0x002160 && c <= 0x002183 || + c >= 0x002336 && c <= 0x00237A || + c >= 0x00249C && c <= 0x0024E9 || + c >= 0x003005 && c <= 0x003007 || + c >= 0x003021 && c <= 0x003029 || + c >= 0x003031 && c <= 0x003035 || + c >= 0x003038 && c <= 0x00303C || + c >= 0x003041 && c <= 0x003096 || + c >= 0x00309D && c <= 0x00309F || + c >= 0x0030A1 && c <= 0x0030FA || + c >= 0x0030FC && c <= 0x0030FF || + c >= 0x003105 && c <= 0x00312C || + c >= 0x003131 && c <= 0x00318E || + c >= 0x003190 && c <= 0x0031B7 || + c >= 0x0031F0 && c <= 0x00321C || + c >= 0x003220 && c <= 0x003243 || + c >= 0x003260 && c <= 0x00327B || + c >= 0x00327F && c <= 0x0032B0 || + c >= 0x0032C0 && c <= 0x0032CB || + c >= 0x0032D0 && c <= 0x0032FE || + c >= 0x003300 && c <= 0x003376 || + c >= 0x00337B && c <= 0x0033DD || + c >= 0x0033E0 && c <= 0x0033FE || + c >= 0x003400 && c <= 0x004DB5 || + c >= 0x004E00 && c <= 0x009FA5 || + c >= 0x00A000 && c <= 0x00A48C || + c >= 0x00AC00 && c <= 0x00D7A3 || + c >= 0x00D800 && c <= 0x00FA2D || + c >= 0x00FA30 && c <= 0x00FA6A || + c >= 0x00FB00 && c <= 0x00FB06 || + c >= 0x00FB13 && c <= 0x00FB17 || + c >= 0x00FF21 && c <= 0x00FF3A || + c >= 0x00FF41 && c <= 0x00FF5A || + c >= 0x00FF66 && c <= 0x00FFBE || + c >= 0x00FFC2 && c <= 0x00FFC7 || + c >= 0x00FFCA && c <= 0x00FFCF || + c >= 0x00FFD2 && c <= 0x00FFD7 || + c >= 0x00FFDA && c <= 0x00FFDC || + c >= 0x010300 && c <= 0x01031E || + c >= 0x010320 && c <= 0x010323 || + c >= 0x010330 && c <= 0x01034A || + c >= 0x010400 && c <= 0x010425 || + c >= 0x010428 && c <= 0x01044D || + c >= 0x01D000 && c <= 0x01D0F5 || + c >= 0x01D100 && c <= 0x01D126 || + c >= 0x01D12A && c <= 0x01D166 || + c >= 0x01D16A && c <= 0x01D172 || + c >= 0x01D183 && c <= 0x01D184 || + c >= 0x01D18C && c <= 0x01D1A9 || + c >= 0x01D1AE && c <= 0x01D1DD || + c >= 0x01D400 && c <= 0x01D454 || + c >= 0x01D456 && c <= 0x01D49C || + c >= 0x01D49E && c <= 0x01D49F || + c >= 0x01D4A5 && c <= 0x01D4A6 || + c >= 0x01D4A9 && c <= 0x01D4AC || + c >= 0x01D4AE && c <= 0x01D4B9 || + c >= 0x01D4BD && c <= 0x01D4C0 || + c >= 0x01D4C2 && c <= 0x01D4C3 || + c >= 0x01D4C5 && c <= 0x01D505 || + c >= 0x01D507 && c <= 0x01D50A || + c >= 0x01D50D && c <= 0x01D514 || + c >= 0x01D516 && c <= 0x01D51C || + c >= 0x01D51E && c <= 0x01D539 || + c >= 0x01D53B && c <= 0x01D53E || + c >= 0x01D540 && c <= 0x01D544 || + c >= 0x01D54A && c <= 0x01D550 || + c >= 0x01D552 && c <= 0x01D6A3 || + c >= 0x01D6A8 && c <= 0x01D7C9 || + c >= 0x020000 && c <= 0x02A6D6 || + c >= 0x02F800 && c <= 0x02FA1D || + c >= 0x0F0000 && c <= 0x0FFFFD || + c >= 0x100000 && c <= 0x10FFFD || + c == 0x0000AA || c == 0x0000B5 || + c == 0x0000BA || c == 0x0002EE || + c == 0x00037A || c == 0x000386 || + c == 0x00038C || c == 0x000589 || + c == 0x000903 || c == 0x000950 || + c == 0x0009B2 || c == 0x0009D7 || + c == 0x000A5E || c == 0x000A83 || + c == 0x000A8D || c == 0x000AC9 || + c == 0x000AD0 || c == 0x000AE0 || + c == 0x000B40 || c == 0x000B57 || + c == 0x000B83 || c == 0x000B9C || + c == 0x000BD7 || c == 0x000CBE || + c == 0x000CDE || c == 0x000D57 || + c == 0x000DBD || c == 0x000E84 || + c == 0x000E8A || c == 0x000E8D || + c == 0x000EA5 || c == 0x000EA7 || + c == 0x000EBD || c == 0x000EC6 || + c == 0x000F36 || c == 0x000F38 || + c == 0x000F7F || c == 0x000F85 || + c == 0x000FCF || c == 0x00102C || + c == 0x001031 || c == 0x001038 || + c == 0x0010FB || c == 0x001248 || + c == 0x001258 || c == 0x001288 || + c == 0x0012B0 || c == 0x0012C0 || + c == 0x001310 || c == 0x0017DC || + c == 0x001F59 || c == 0x001F5B || + c == 0x001F5D || c == 0x001FBE || + c == 0x00200E || c == 0x002071 || + c == 0x00207F || c == 0x002102 || + c == 0x002107 || c == 0x002115 || + c == 0x002124 || c == 0x002126 || + c == 0x002128 || c == 0x002395 || + c == 0x01D4A2 || c == 0x01D4BB || + c == 0x01D546; + } - return number.ToString(CultureInfo.InvariantCulture); + // Used by ListExtraItemParser to format numbers from 1 - 26 + private static readonly string[] smallNumberStringCache = { + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", + "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", + "20", "21", "22", "23", "24", "25", "26", + }; + + internal static string SmallNumberToString(int number) + { + string[] cache = smallNumberStringCache; + if ((uint)number < (uint)cache.Length) + { + return cache[number]; } + + return number.ToString(CultureInfo.InvariantCulture); } } \ No newline at end of file diff --git a/src/Markdig/Helpers/CharNormalizer.cs b/src/Markdig/Helpers/CharNormalizer.cs index 82ab74652..fc6ecffa9 100644 --- a/src/Markdig/Helpers/CharNormalizer.cs +++ b/src/Markdig/Helpers/CharNormalizer.cs @@ -2,1297 +2,1294 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using System.Collections.Generic; +namespace Markdig.Helpers; -namespace Markdig.Helpers +/// +/// Class used to simplify a unicode char to a simple ASCII string +/// +public static class CharNormalizer { /// - /// Class used to simplify a unicode char to a simple ASCII string + /// Converts a unicode char to a simple ASCII string. /// - public static class CharNormalizer + /// The input char. + /// The simple ASCII string or null if the char itself cannot be simplified + public static string? ConvertToAscii(char c) { - /// - /// Converts a unicode char to a simple ASCII string. - /// - /// The input char. - /// The simple ASCII string or null if the char itself cannot be simplified - public static string? ConvertToAscii(char c) - { - return c >= 160 && CodeToAscii.TryGetValue(c, out string? str) ? str : null; - } - - // This table was generated by the app UnicodeNormDApp - private static readonly Dictionary CodeToAscii = new(1269) - { - {'Ḋ', "D"}, - {'Ḍ', "D"}, - {'È', "E"}, - {'Ē', "E"}, - {'Ḕ', "E"}, - {'ª', "a"}, - {'²', "2"}, - {'³', "3"}, - {'¹', "1"}, - {'º', "o"}, - {'¼', "14"}, - {'½', "12"}, - {'¾', "34"}, - {'À', "A"}, - {'Á', "A"}, - {'Â', "A"}, - {'Ã', "A"}, - {'Ä', "A"}, - {'Å', "A"}, - {'Ç', "C"}, - {'É', "E"}, - {'Ê', "E"}, - {'Ë', "E"}, - {'Ì', "I"}, - {'Í', "I"}, - {'Î', "I"}, - {'Ï', "I"}, - {'Ñ', "N"}, - {'Ò', "O"}, - {'Ó', "O"}, - {'Ô', "O"}, - {'Õ', "O"}, - {'Ö', "O"}, - {'Ù', "U"}, - {'Ú', "U"}, - {'Û', "U"}, - {'Ü', "U"}, - {'Ý', "Y"}, - {'à', "a"}, - {'á', "a"}, - {'â', "a"}, - {'ã', "a"}, - {'ä', "a"}, - {'å', "a"}, - {'ç', "c"}, - {'è', "e"}, - {'é', "e"}, - {'ê', "e"}, - {'ë', "e"}, - {'ì', "i"}, - {'í', "i"}, - {'î', "i"}, - {'ï', "i"}, - {'ñ', "n"}, - {'ò', "o"}, - {'ó', "o"}, - {'ô', "o"}, - {'õ', "o"}, - {'ö', "o"}, - {'ù', "u"}, - {'ú', "u"}, - {'û', "u"}, - {'ü', "u"}, - {'ý', "y"}, - {'ÿ', "y"}, - {'Ā', "A"}, - {'ā', "a"}, - {'Ă', "A"}, - {'ă', "a"}, - {'Ą', "A"}, - {'ą', "a"}, - {'Ć', "C"}, - {'ć', "c"}, - {'Ĉ', "C"}, - {'ĉ', "c"}, - {'Ċ', "C"}, - {'ċ', "c"}, - {'Č', "C"}, - {'č', "c"}, - {'Ď', "D"}, - {'ď', "d"}, - {'ē', "e"}, - {'Ĕ', "E"}, - {'ĕ', "e"}, - {'Ė', "E"}, - {'ė', "e"}, - {'Ę', "E"}, - {'ę', "e"}, - {'Ě', "E"}, - {'ě', "e"}, - {'Ĝ', "G"}, - {'ĝ', "g"}, - {'Ğ', "G"}, - {'ğ', "g"}, - {'Ġ', "G"}, - {'ġ', "g"}, - {'Ģ', "G"}, - {'ģ', "g"}, - {'Ĥ', "H"}, - {'ĥ', "h"}, - {'Ĩ', "I"}, - {'ĩ', "i"}, - {'Ī', "I"}, - {'ī', "i"}, - {'Ĭ', "I"}, - {'ĭ', "i"}, - {'Į', "I"}, - {'į', "i"}, - {'İ', "I"}, - {'IJ', "IJ"}, - {'ij', "ij"}, - {'Ĵ', "J"}, - {'ĵ', "j"}, - {'Ķ', "K"}, - {'ķ', "k"}, - {'Ĺ', "L"}, - {'ĺ', "l"}, - {'Ļ', "L"}, - {'ļ', "l"}, - {'Ľ', "L"}, - {'ľ', "l"}, - {'Ŀ', "L"}, - {'ŀ', "l"}, - {'Ń', "N"}, - {'ń', "n"}, - {'Ņ', "N"}, - {'ņ', "n"}, - {'Ň', "N"}, - {'ň', "n"}, - {'ʼn', "n"}, - {'Ō', "O"}, - {'ō', "o"}, - {'Ŏ', "O"}, - {'ŏ', "o"}, - {'Ő', "O"}, - {'ő', "o"}, - {'Ŕ', "R"}, - {'ŕ', "r"}, - {'Ŗ', "R"}, - {'ŗ', "r"}, - {'Ř', "R"}, - {'ř', "r"}, - {'Ś', "S"}, - {'ś', "s"}, - {'Ŝ', "S"}, - {'ŝ', "s"}, - {'Ş', "S"}, - {'ş', "s"}, - {'Š', "S"}, - {'š', "s"}, - {'Ţ', "T"}, - {'ţ', "t"}, - {'Ť', "T"}, - {'ť', "t"}, - {'Ũ', "U"}, - {'ũ', "u"}, - {'Ū', "U"}, - {'ū', "u"}, - {'Ŭ', "U"}, - {'ŭ', "u"}, - {'Ů', "U"}, - {'ů', "u"}, - {'Ű', "U"}, - {'ű', "u"}, - {'Ų', "U"}, - {'ų', "u"}, - {'Ŵ', "W"}, - {'ŵ', "w"}, - {'Ŷ', "Y"}, - {'ŷ', "y"}, - {'Ÿ', "Y"}, - {'Ź', "Z"}, - {'ź', "z"}, - {'Ż', "Z"}, - {'ż', "z"}, - {'Ž', "Z"}, - {'ž', "z"}, - {'ſ', "s"}, - {'Ơ', "O"}, - {'ơ', "o"}, - {'Ư', "U"}, - {'ư', "u"}, - {'DŽ', "DZ"}, - {'Dž', "Dz"}, - {'dž', "dz"}, - {'LJ', "LJ"}, - {'Lj', "Lj"}, - {'lj', "lj"}, - {'NJ', "NJ"}, - {'Nj', "Nj"}, - {'nj', "nj"}, - {'Ǎ', "A"}, - {'ǎ', "a"}, - {'Ǐ', "I"}, - {'ǐ', "i"}, - {'Ǒ', "O"}, - {'ǒ', "o"}, - {'Ǔ', "U"}, - {'ǔ', "u"}, - {'Ǖ', "U"}, - {'ǖ', "u"}, - {'Ǘ', "U"}, - {'ǘ', "u"}, - {'Ǚ', "U"}, - {'ǚ', "u"}, - {'Ǜ', "U"}, - {'ǜ', "u"}, - {'Ǟ', "A"}, - {'ǟ', "a"}, - {'Ǡ', "A"}, - {'ǡ', "a"}, - {'Ǧ', "G"}, - {'ǧ', "g"}, - {'Ǩ', "K"}, - {'ǩ', "k"}, - {'Ǫ', "O"}, - {'ǫ', "o"}, - {'Ǭ', "O"}, - {'ǭ', "o"}, - {'ǰ', "j"}, - {'DZ', "DZ"}, - {'Dz', "Dz"}, - {'dz', "dz"}, - {'Ǵ', "G"}, - {'ǵ', "g"}, - {'Ǹ', "N"}, - {'ǹ', "n"}, - {'Ǻ', "A"}, - {'ǻ', "a"}, - {'Ȁ', "A"}, - {'ȁ', "a"}, - {'Ȃ', "A"}, - {'ȃ', "a"}, - {'Ȅ', "E"}, - {'ȅ', "e"}, - {'Ȇ', "E"}, - {'ȇ', "e"}, - {'Ȉ', "I"}, - {'ȉ', "i"}, - {'Ȋ', "I"}, - {'ȋ', "i"}, - {'Ȍ', "O"}, - {'ȍ', "o"}, - {'Ȏ', "O"}, - {'ȏ', "o"}, - {'Ȑ', "R"}, - {'ȑ', "r"}, - {'Ȓ', "R"}, - {'ȓ', "r"}, - {'Ȕ', "U"}, - {'ȕ', "u"}, - {'Ȗ', "U"}, - {'ȗ', "u"}, - {'Ș', "S"}, - {'ș', "s"}, - {'Ț', "T"}, - {'ț', "t"}, - {'Ȟ', "H"}, - {'ȟ', "h"}, - {'Ȧ', "A"}, - {'ȧ', "a"}, - {'Ȩ', "E"}, - {'ȩ', "e"}, - {'Ȫ', "O"}, - {'ȫ', "o"}, - {'Ȭ', "O"}, - {'ȭ', "o"}, - {'Ȯ', "O"}, - {'ȯ', "o"}, - {'Ȱ', "O"}, - {'ȱ', "o"}, - {'Ȳ', "Y"}, - {'ȳ', "y"}, - {'ʰ', "h"}, - {'ʲ', "j"}, - {'ʳ', "r"}, - {'ʷ', "w"}, - {'ʸ', "y"}, - {'ˡ', "l"}, - {'ˢ', "s"}, - {'ˣ', "x"}, - {';', ";"}, - {'ᴬ', "A"}, - {'ᴮ', "B"}, - {'ᴰ', "D"}, - {'ᴱ', "E"}, - {'ᴳ', "G"}, - {'ᴴ', "H"}, - {'ᴵ', "I"}, - {'ᴶ', "J"}, - {'ᴷ', "K"}, - {'ᴸ', "L"}, - {'ᴹ', "M"}, - {'ᴺ', "N"}, - {'ᴼ', "O"}, - {'ᴾ', "P"}, - {'ᴿ', "R"}, - {'ᵀ', "T"}, - {'ᵁ', "U"}, - {'ᵂ', "W"}, - {'ᵃ', "a"}, - {'ᵇ', "b"}, - {'ᵈ', "d"}, - {'ᵉ', "e"}, - {'ᵍ', "g"}, - {'ᵏ', "k"}, - {'ᵐ', "m"}, - {'ᵒ', "o"}, - {'ᵖ', "p"}, - {'ᵗ', "t"}, - {'ᵘ', "u"}, - {'ᵛ', "v"}, - {'ᵢ', "i"}, - {'ᵣ', "r"}, - {'ᵤ', "u"}, - {'ᵥ', "v"}, - {'ᶜ', "c"}, - {'ᶠ', "f"}, - {'ᶻ', "z"}, - {'Ḁ', "A"}, - {'ḁ', "a"}, - {'Ḃ', "B"}, - {'ḃ', "b"}, - {'Ḅ', "B"}, - {'ḅ', "b"}, - {'Ḇ', "B"}, - {'ḇ', "b"}, - {'Ḉ', "C"}, - {'ḉ', "c"}, - {'ḋ', "d"}, - {'ḍ', "d"}, - {'Ḏ', "D"}, - {'ḏ', "d"}, - {'Ḑ', "D"}, - {'ḑ', "d"}, - {'Ḓ', "D"}, - {'ḓ', "d"}, - {'ḕ', "e"}, - {'Ḗ', "E"}, - {'ḗ', "e"}, - {'Ḙ', "E"}, - {'ḙ', "e"}, - {'Ḛ', "E"}, - {'ḛ', "e"}, - {'Ḝ', "E"}, - {'ḝ', "e"}, - {'Ḟ', "F"}, - {'ḟ', "f"}, - {'Ḡ', "G"}, - {'ḡ', "g"}, - {'Ḣ', "H"}, - {'ḣ', "h"}, - {'Ḥ', "H"}, - {'ḥ', "h"}, - {'Ḧ', "H"}, - {'ḧ', "h"}, - {'Ḩ', "H"}, - {'ḩ', "h"}, - {'Ḫ', "H"}, - {'ḫ', "h"}, - {'Ḭ', "I"}, - {'ḭ', "i"}, - {'Ḯ', "I"}, - {'ḯ', "i"}, - {'Ḱ', "K"}, - {'ḱ', "k"}, - {'Ḳ', "K"}, - {'ḳ', "k"}, - {'Ḵ', "K"}, - {'ḵ', "k"}, - {'Ḷ', "L"}, - {'ḷ', "l"}, - {'Ḹ', "L"}, - {'ḹ', "l"}, - {'Ḻ', "L"}, - {'ḻ', "l"}, - {'Ḽ', "L"}, - {'ḽ', "l"}, - {'Ḿ', "M"}, - {'ḿ', "m"}, - {'Ṁ', "M"}, - {'ṁ', "m"}, - {'Ṃ', "M"}, - {'ṃ', "m"}, - {'Ṅ', "N"}, - {'ṅ', "n"}, - {'Ṇ', "N"}, - {'ṇ', "n"}, - {'Ṉ', "N"}, - {'ṉ', "n"}, - {'Ṋ', "N"}, - {'ṋ', "n"}, - {'Ṍ', "O"}, - {'ṍ', "o"}, - {'Ṏ', "O"}, - {'ṏ', "o"}, - {'Ṑ', "O"}, - {'ṑ', "o"}, - {'Ṓ', "O"}, - {'ṓ', "o"}, - {'Ṕ', "P"}, - {'ṕ', "p"}, - {'Ṗ', "P"}, - {'ṗ', "p"}, - {'Ṙ', "R"}, - {'ṙ', "r"}, - {'Ṛ', "R"}, - {'ṛ', "r"}, - {'Ṝ', "R"}, - {'ṝ', "r"}, - {'Ṟ', "R"}, - {'ṟ', "r"}, - {'Ṡ', "S"}, - {'ṡ', "s"}, - {'Ṣ', "S"}, - {'ṣ', "s"}, - {'Ṥ', "S"}, - {'ṥ', "s"}, - {'Ṧ', "S"}, - {'ṧ', "s"}, - {'Ṩ', "S"}, - {'ṩ', "s"}, - {'Ṫ', "T"}, - {'ṫ', "t"}, - {'Ṭ', "T"}, - {'ṭ', "t"}, - {'Ṯ', "T"}, - {'ṯ', "t"}, - {'Ṱ', "T"}, - {'ṱ', "t"}, - {'Ṳ', "U"}, - {'ṳ', "u"}, - {'Ṵ', "U"}, - {'ṵ', "u"}, - {'Ṷ', "U"}, - {'ṷ', "u"}, - {'Ṹ', "U"}, - {'ṹ', "u"}, - {'Ṻ', "U"}, - {'ṻ', "u"}, - {'Ṽ', "V"}, - {'ṽ', "v"}, - {'Ṿ', "V"}, - {'ṿ', "v"}, - {'Ẁ', "W"}, - {'ẁ', "w"}, - {'Ẃ', "W"}, - {'ẃ', "w"}, - {'Ẅ', "W"}, - {'ẅ', "w"}, - {'Ẇ', "W"}, - {'ẇ', "w"}, - {'Ẉ', "W"}, - {'ẉ', "w"}, - {'Ẋ', "X"}, - {'ẋ', "x"}, - {'Ẍ', "X"}, - {'ẍ', "x"}, - {'Ẏ', "Y"}, - {'ẏ', "y"}, - {'Ẑ', "Z"}, - {'ẑ', "z"}, - {'Ẓ', "Z"}, - {'ẓ', "z"}, - {'Ẕ', "Z"}, - {'ẕ', "z"}, - {'ẖ', "h"}, - {'ẗ', "t"}, - {'ẘ', "w"}, - {'ẙ', "y"}, - {'ẚ', "a"}, - {'ẛ', "s"}, - {'Ạ', "A"}, - {'ạ', "a"}, - {'Ả', "A"}, - {'ả', "a"}, - {'Ấ', "A"}, - {'ấ', "a"}, - {'Ầ', "A"}, - {'ầ', "a"}, - {'Ẩ', "A"}, - {'ẩ', "a"}, - {'Ẫ', "A"}, - {'ẫ', "a"}, - {'Ậ', "A"}, - {'ậ', "a"}, - {'Ắ', "A"}, - {'ắ', "a"}, - {'Ằ', "A"}, - {'ằ', "a"}, - {'Ẳ', "A"}, - {'ẳ', "a"}, - {'Ẵ', "A"}, - {'ẵ', "a"}, - {'Ặ', "A"}, - {'ặ', "a"}, - {'Ẹ', "E"}, - {'ẹ', "e"}, - {'Ẻ', "E"}, - {'ẻ', "e"}, - {'Ẽ', "E"}, - {'ẽ', "e"}, - {'Ế', "E"}, - {'ế', "e"}, - {'Ề', "E"}, - {'ề', "e"}, - {'Ể', "E"}, - {'ể', "e"}, - {'Ễ', "E"}, - {'ễ', "e"}, - {'Ệ', "E"}, - {'ệ', "e"}, - {'Ỉ', "I"}, - {'ỉ', "i"}, - {'Ị', "I"}, - {'ị', "i"}, - {'Ọ', "O"}, - {'ọ', "o"}, - {'Ỏ', "O"}, - {'ỏ', "o"}, - {'Ố', "O"}, - {'ố', "o"}, - {'Ồ', "O"}, - {'ồ', "o"}, - {'Ổ', "O"}, - {'ổ', "o"}, - {'Ỗ', "O"}, - {'ỗ', "o"}, - {'Ộ', "O"}, - {'ộ', "o"}, - {'Ớ', "O"}, - {'ớ', "o"}, - {'Ờ', "O"}, - {'ờ', "o"}, - {'Ở', "O"}, - {'ở', "o"}, - {'Ỡ', "O"}, - {'ỡ', "o"}, - {'Ợ', "O"}, - {'ợ', "o"}, - {'Ụ', "U"}, - {'ụ', "u"}, - {'Ủ', "U"}, - {'ủ', "u"}, - {'Ứ', "U"}, - {'ứ', "u"}, - {'Ừ', "U"}, - {'ừ', "u"}, - {'Ử', "U"}, - {'ử', "u"}, - {'Ữ', "U"}, - {'ữ', "u"}, - {'Ự', "U"}, - {'ự', "u"}, - {'Ỳ', "Y"}, - {'ỳ', "y"}, - {'Ỵ', "Y"}, - {'ỵ', "y"}, - {'Ỷ', "Y"}, - {'ỷ', "y"}, - {'Ỹ', "Y"}, - {'ỹ', "y"}, - {'`', "`"}, - {'․', "."}, - {'‥', ".."}, - {'…', "..."}, - {'‼', "!!"}, - {'⁇', "??"}, - {'⁈', "?!"}, - {'⁉', "!?"}, - {'⁰', "0"}, - {'ⁱ', "i"}, - {'⁴', "4"}, - {'⁵', "5"}, - {'⁶', "6"}, - {'⁷', "7"}, - {'⁸', "8"}, - {'⁹', "9"}, - {'⁺', "+"}, - {'⁼', "="}, - {'⁽', "("}, - {'⁾', ")"}, - {'ⁿ', "n"}, - {'₀', "0"}, - {'₁', "1"}, - {'₂', "2"}, - {'₃', "3"}, - {'₄', "4"}, - {'₅', "5"}, - {'₆', "6"}, - {'₇', "7"}, - {'₈', "8"}, - {'₉', "9"}, - {'₊', "+"}, - {'₌', "="}, - {'₍', "("}, - {'₎', ")"}, - {'ₐ', "a"}, - {'ₑ', "e"}, - {'ₒ', "o"}, - {'ₓ', "x"}, - {'ₕ', "h"}, - {'ₖ', "k"}, - {'ₗ', "l"}, - {'ₘ', "m"}, - {'ₙ', "n"}, - {'ₚ', "p"}, - {'ₛ', "s"}, - {'ₜ', "t"}, - {'₨', "Rs"}, - {'℀', "a/c"}, - {'℁', "a/s"}, - {'ℂ', "C"}, - {'℃', "C"}, - {'℅', "c/o"}, - {'℆', "c/u"}, - {'℉', "F"}, - {'ℊ', "g"}, - {'ℋ', "H"}, - {'ℌ', "H"}, - {'ℍ', "H"}, - {'ℎ', "h"}, - {'ℐ', "I"}, - {'ℑ', "I"}, - {'ℒ', "L"}, - {'ℓ', "l"}, - {'ℕ', "N"}, - {'№', "No"}, - {'ℙ', "P"}, - {'ℚ', "Q"}, - {'ℛ', "R"}, - {'ℜ', "R"}, - {'ℝ', "R"}, - {'℠', "SM"}, - {'℡', "TEL"}, - {'™', "TM"}, - {'ℤ', "Z"}, - {'ℨ', "Z"}, - {'K', "K"}, - {'Å', "A"}, - {'ℬ', "B"}, - {'ℭ', "C"}, - {'ℯ', "e"}, - {'ℰ', "E"}, - {'ℱ', "F"}, - {'ℳ', "M"}, - {'ℴ', "o"}, - {'ℹ', "i"}, - {'℻', "FAX"}, - {'ⅅ', "D"}, - {'ⅆ', "d"}, - {'ⅇ', "e"}, - {'ⅈ', "i"}, - {'ⅉ', "j"}, - {'⅐', "17"}, - {'⅑', "19"}, - {'⅒', "110"}, - {'⅓', "13"}, - {'⅔', "23"}, - {'⅕', "15"}, - {'⅖', "25"}, - {'⅗', "35"}, - {'⅘', "45"}, - {'⅙', "16"}, - {'⅚', "56"}, - {'⅛', "18"}, - {'⅜', "38"}, - {'⅝', "58"}, - {'⅞', "78"}, - {'⅟', "1"}, - {'Ⅰ', "I"}, - {'Ⅱ', "II"}, - {'Ⅲ', "III"}, - {'Ⅳ', "IV"}, - {'Ⅴ', "V"}, - {'Ⅵ', "VI"}, - {'Ⅶ', "VII"}, - {'Ⅷ', "VIII"}, - {'Ⅸ', "IX"}, - {'Ⅹ', "X"}, - {'Ⅺ', "XI"}, - {'Ⅻ', "XII"}, - {'Ⅼ', "L"}, - {'Ⅽ', "C"}, - {'Ⅾ', "D"}, - {'Ⅿ', "M"}, - {'ⅰ', "i"}, - {'ⅱ', "ii"}, - {'ⅲ', "iii"}, - {'ⅳ', "iv"}, - {'ⅴ', "v"}, - {'ⅵ', "vi"}, - {'ⅶ', "vii"}, - {'ⅷ', "viii"}, - {'ⅸ', "ix"}, - {'ⅹ', "x"}, - {'ⅺ', "xi"}, - {'ⅻ', "xii"}, - {'ⅼ', "l"}, - {'ⅽ', "c"}, - {'ⅾ', "d"}, - {'ⅿ', "m"}, - {'↉', "03"}, - {'≠', "="}, - {'≮', "<"}, - {'≯', ">"}, - {'①', "1"}, - {'②', "2"}, - {'③', "3"}, - {'④', "4"}, - {'⑤', "5"}, - {'⑥', "6"}, - {'⑦', "7"}, - {'⑧', "8"}, - {'⑨', "9"}, - {'⑩', "10"}, - {'⑪', "11"}, - {'⑫', "12"}, - {'⑬', "13"}, - {'⑭', "14"}, - {'⑮', "15"}, - {'⑯', "16"}, - {'⑰', "17"}, - {'⑱', "18"}, - {'⑲', "19"}, - {'⑳', "20"}, - {'⑴', "(1)"}, - {'⑵', "(2)"}, - {'⑶', "(3)"}, - {'⑷', "(4)"}, - {'⑸', "(5)"}, - {'⑹', "(6)"}, - {'⑺', "(7)"}, - {'⑻', "(8)"}, - {'⑼', "(9)"}, - {'⑽', "(10)"}, - {'⑾', "(11)"}, - {'⑿', "(12)"}, - {'⒀', "(13)"}, - {'⒁', "(14)"}, - {'⒂', "(15)"}, - {'⒃', "(16)"}, - {'⒄', "(17)"}, - {'⒅', "(18)"}, - {'⒆', "(19)"}, - {'⒇', "(20)"}, - {'⒈', "1."}, - {'⒉', "2."}, - {'⒊', "3."}, - {'⒋', "4."}, - {'⒌', "5."}, - {'⒍', "6."}, - {'⒎', "7."}, - {'⒏', "8."}, - {'⒐', "9."}, - {'⒑', "10."}, - {'⒒', "11."}, - {'⒓', "12."}, - {'⒔', "13."}, - {'⒕', "14."}, - {'⒖', "15."}, - {'⒗', "16."}, - {'⒘', "17."}, - {'⒙', "18."}, - {'⒚', "19."}, - {'⒛', "20."}, - {'⒜', "(a)"}, - {'⒝', "(b)"}, - {'⒞', "(c)"}, - {'⒟', "(d)"}, - {'⒠', "(e)"}, - {'⒡', "(f)"}, - {'⒢', "(g)"}, - {'⒣', "(h)"}, - {'⒤', "(i)"}, - {'⒥', "(j)"}, - {'⒦', "(k)"}, - {'⒧', "(l)"}, - {'⒨', "(m)"}, - {'⒩', "(n)"}, - {'⒪', "(o)"}, - {'⒫', "(p)"}, - {'⒬', "(q)"}, - {'⒭', "(r)"}, - {'⒮', "(s)"}, - {'⒯', "(t)"}, - {'⒰', "(u)"}, - {'⒱', "(v)"}, - {'⒲', "(w)"}, - {'⒳', "(x)"}, - {'⒴', "(y)"}, - {'⒵', "(z)"}, - {'Ⓐ', "A"}, - {'Ⓑ', "B"}, - {'Ⓒ', "C"}, - {'Ⓓ', "D"}, - {'Ⓔ', "E"}, - {'Ⓕ', "F"}, - {'Ⓖ', "G"}, - {'Ⓗ', "H"}, - {'Ⓘ', "I"}, - {'Ⓙ', "J"}, - {'Ⓚ', "K"}, - {'Ⓛ', "L"}, - {'Ⓜ', "M"}, - {'Ⓝ', "N"}, - {'Ⓞ', "O"}, - {'Ⓟ', "P"}, - {'Ⓠ', "Q"}, - {'Ⓡ', "R"}, - {'Ⓢ', "S"}, - {'Ⓣ', "T"}, - {'Ⓤ', "U"}, - {'Ⓥ', "V"}, - {'Ⓦ', "W"}, - {'Ⓧ', "X"}, - {'Ⓨ', "Y"}, - {'Ⓩ', "Z"}, - {'ⓐ', "a"}, - {'ⓑ', "b"}, - {'ⓒ', "c"}, - {'ⓓ', "d"}, - {'ⓔ', "e"}, - {'ⓕ', "f"}, - {'ⓖ', "g"}, - {'ⓗ', "h"}, - {'ⓘ', "i"}, - {'ⓙ', "j"}, - {'ⓚ', "k"}, - {'ⓛ', "l"}, - {'ⓜ', "m"}, - {'ⓝ', "n"}, - {'ⓞ', "o"}, - {'ⓟ', "p"}, - {'ⓠ', "q"}, - {'ⓡ', "r"}, - {'ⓢ', "s"}, - {'ⓣ', "t"}, - {'ⓤ', "u"}, - {'ⓥ', "v"}, - {'ⓦ', "w"}, - {'ⓧ', "x"}, - {'ⓨ', "y"}, - {'ⓩ', "z"}, - {'⓪', "0"}, - {'⩴', "::="}, - {'⩵', "=="}, - {'⩶', "==="}, - {'ⱼ', "j"}, - {'ⱽ', "V"}, - {'㈀', "()"}, - {'㈁', "()"}, - {'㈂', "()"}, - {'㈃', "()"}, - {'㈄', "()"}, - {'㈅', "()"}, - {'㈆', "()"}, - {'㈇', "()"}, - {'㈈', "()"}, - {'㈉', "()"}, - {'㈊', "()"}, - {'㈋', "()"}, - {'㈌', "()"}, - {'㈍', "()"}, - {'㈎', "()"}, - {'㈏', "()"}, - {'㈐', "()"}, - {'㈑', "()"}, - {'㈒', "()"}, - {'㈓', "()"}, - {'㈔', "()"}, - {'㈕', "()"}, - {'㈖', "()"}, - {'㈗', "()"}, - {'㈘', "()"}, - {'㈙', "()"}, - {'㈚', "()"}, - {'㈛', "()"}, - {'㈜', "()"}, - {'㈝', "()"}, - {'㈞', "()"}, - {'㈠', "()"}, - {'㈡', "()"}, - {'㈢', "()"}, - {'㈣', "()"}, - {'㈤', "()"}, - {'㈥', "()"}, - {'㈦', "()"}, - {'㈧', "()"}, - {'㈨', "()"}, - {'㈩', "()"}, - {'㈪', "()"}, - {'㈫', "()"}, - {'㈬', "()"}, - {'㈭', "()"}, - {'㈮', "()"}, - {'㈯', "()"}, - {'㈰', "()"}, - {'㈱', "()"}, - {'㈲', "()"}, - {'㈳', "()"}, - {'㈴', "()"}, - {'㈵', "()"}, - {'㈶', "()"}, - {'㈷', "()"}, - {'㈸', "()"}, - {'㈹', "()"}, - {'㈺', "()"}, - {'㈻', "()"}, - {'㈼', "()"}, - {'㈽', "()"}, - {'㈾', "()"}, - {'㈿', "()"}, - {'㉀', "()"}, - {'㉁', "()"}, - {'㉂', "()"}, - {'㉃', "()"}, - {'㉐', "PTE"}, - {'㉑', "21"}, - {'㉒', "22"}, - {'㉓', "23"}, - {'㉔', "24"}, - {'㉕', "25"}, - {'㉖', "26"}, - {'㉗', "27"}, - {'㉘', "28"}, - {'㉙', "29"}, - {'㉚', "30"}, - {'㉛', "31"}, - {'㉜', "32"}, - {'㉝', "33"}, - {'㉞', "34"}, - {'㉟', "35"}, - {'㊱', "36"}, - {'㊲', "37"}, - {'㊳', "38"}, - {'㊴', "39"}, - {'㊵', "40"}, - {'㊶', "41"}, - {'㊷', "42"}, - {'㊸', "43"}, - {'㊹', "44"}, - {'㊺', "45"}, - {'㊻', "46"}, - {'㊼', "47"}, - {'㊽', "48"}, - {'㊾', "49"}, - {'㊿', "50"}, - {'㋀', "1"}, - {'㋁', "2"}, - {'㋂', "3"}, - {'㋃', "4"}, - {'㋄', "5"}, - {'㋅', "6"}, - {'㋆', "7"}, - {'㋇', "8"}, - {'㋈', "9"}, - {'㋉', "10"}, - {'㋊', "11"}, - {'㋋', "12"}, - {'㋌', "Hg"}, - {'㋍', "erg"}, - {'㋎', "eV"}, - {'㋏', "LTD"}, - {'㍘', "0"}, - {'㍙', "1"}, - {'㍚', "2"}, - {'㍛', "3"}, - {'㍜', "4"}, - {'㍝', "5"}, - {'㍞', "6"}, - {'㍟', "7"}, - {'㍠', "8"}, - {'㍡', "9"}, - {'㍢', "10"}, - {'㍣', "11"}, - {'㍤', "12"}, - {'㍥', "13"}, - {'㍦', "14"}, - {'㍧', "15"}, - {'㍨', "16"}, - {'㍩', "17"}, - {'㍪', "18"}, - {'㍫', "19"}, - {'㍬', "20"}, - {'㍭', "21"}, - {'㍮', "22"}, - {'㍯', "23"}, - {'㍰', "24"}, - {'㍱', "hPa"}, - {'㍲', "da"}, - {'㍳', "AU"}, - {'㍴', "bar"}, - {'㍵', "oV"}, - {'㍶', "pc"}, - {'㍷', "dm"}, - {'㍸', "dm2"}, - {'㍹', "dm3"}, - {'㍺', "IU"}, - {'㎀', "pA"}, - {'㎁', "nA"}, - {'㎂', "A"}, - {'㎃', "mA"}, - {'㎄', "kA"}, - {'㎅', "KB"}, - {'㎆', "MB"}, - {'㎇', "GB"}, - {'㎈', "cal"}, - {'㎉', "kcal"}, - {'㎊', "pF"}, - {'㎋', "nF"}, - {'㎌', "F"}, - {'㎍', "g"}, - {'㎎', "mg"}, - {'㎏', "kg"}, - {'㎐', "Hz"}, - {'㎑', "kHz"}, - {'㎒', "MHz"}, - {'㎓', "GHz"}, - {'㎔', "THz"}, - {'㎕', "l"}, - {'㎖', "ml"}, - {'㎗', "dl"}, - {'㎘', "kl"}, - {'㎙', "fm"}, - {'㎚', "nm"}, - {'㎛', "m"}, - {'㎜', "mm"}, - {'㎝', "cm"}, - {'㎞', "km"}, - {'㎟', "mm2"}, - {'㎠', "cm2"}, - {'㎡', "m2"}, - {'㎢', "km2"}, - {'㎣', "mm3"}, - {'㎤', "cm3"}, - {'㎥', "m3"}, - {'㎦', "km3"}, - {'㎧', "ms"}, - {'㎨', "ms2"}, - {'㎩', "Pa"}, - {'㎪', "kPa"}, - {'㎫', "MPa"}, - {'㎬', "GPa"}, - {'㎭', "rad"}, - {'㎮', "rads"}, - {'㎯', "rads2"}, - {'㎰', "ps"}, - {'㎱', "ns"}, - {'㎲', "s"}, - {'㎳', "ms"}, - {'㎴', "pV"}, - {'㎵', "nV"}, - {'㎶', "V"}, - {'㎷', "mV"}, - {'㎸', "kV"}, - {'㎹', "MV"}, - {'㎺', "pW"}, - {'㎻', "nW"}, - {'㎼', "W"}, - {'㎽', "mW"}, - {'㎾', "kW"}, - {'㎿', "MW"}, - {'㏀', "k"}, - {'㏁', "M"}, - {'㏂', "a.m."}, - {'㏃', "Bq"}, - {'㏄', "cc"}, - {'㏅', "cd"}, - {'㏆', "Ckg"}, - {'㏇', "Co."}, - {'㏈', "dB"}, - {'㏉', "Gy"}, - {'㏊', "ha"}, - {'㏋', "HP"}, - {'㏌', "in"}, - {'㏍', "KK"}, - {'㏎', "KM"}, - {'㏏', "kt"}, - {'㏐', "lm"}, - {'㏑', "ln"}, - {'㏒', "log"}, - {'㏓', "lx"}, - {'㏔', "mb"}, - {'㏕', "mil"}, - {'㏖', "mol"}, - {'㏗', "PH"}, - {'㏘', "p.m."}, - {'㏙', "PPM"}, - {'㏚', "PR"}, - {'㏛', "sr"}, - {'㏜', "Sv"}, - {'㏝', "Wb"}, - {'㏞', "Vm"}, - {'㏟', "Am"}, - {'㏠', "1"}, - {'㏡', "2"}, - {'㏢', "3"}, - {'㏣', "4"}, - {'㏤', "5"}, - {'㏥', "6"}, - {'㏦', "7"}, - {'㏧', "8"}, - {'㏨', "9"}, - {'㏩', "10"}, - {'㏪', "11"}, - {'㏫', "12"}, - {'㏬', "13"}, - {'㏭', "14"}, - {'㏮', "15"}, - {'㏯', "16"}, - {'㏰', "17"}, - {'㏱', "18"}, - {'㏲', "19"}, - {'㏳', "20"}, - {'㏴', "21"}, - {'㏵', "22"}, - {'㏶', "23"}, - {'㏷', "24"}, - {'㏸', "25"}, - {'㏹', "26"}, - {'㏺', "27"}, - {'㏻', "28"}, - {'㏼', "29"}, - {'㏽', "30"}, - {'㏾', "31"}, - {'㏿', "gal"}, - {'ff', "ff"}, - {'fi', "fi"}, - {'fl', "fl"}, - {'ffi', "ffi"}, - {'ffl', "ffl"}, - {'ſt', "st"}, - {'st', "st"}, - {'﬩', "+"}, - {'︐', ","}, - {'︓', ":"}, - {'︔', ";"}, - {'︕', "!"}, - {'︖', "?"}, - {'︙', "..."}, - {'︰', ".."}, - {'︳', "_"}, - {'︴', "_"}, - {'︵', "("}, - {'︶', ")"}, - {'︷', "{"}, - {'︸', "}"}, - {'﹇', "["}, - {'﹈', "]"}, - {'﹍', "_"}, - {'﹎', "_"}, - {'﹏', "_"}, - {'﹐', ","}, - {'﹒', "."}, - {'﹔', ";"}, - {'﹕', ":"}, - {'﹖', "?"}, - {'﹗', "!"}, - {'﹙', "("}, - {'﹚', ")"}, - {'﹛', "{"}, - {'﹜', "}"}, - {'﹟', "#"}, - {'﹠', "&"}, - {'﹡', "*"}, - {'﹢', "+"}, - {'﹣', "-"}, - {'﹤', "<"}, - {'﹥', ">"}, - {'﹦', "="}, - {'﹨', "\\"}, - {'﹩', "$"}, - {'﹪', "%"}, - {'﹫', "@"}, - {'!', "!"}, - {'"', "\""}, - {'#', "#"}, - {'$', "$"}, - {'%', "%"}, - {'&', "&"}, - {''', "'"}, - {'(', "("}, - {')', ")"}, - {'*', "*"}, - {'+', "+"}, - {',', ","}, - {'-', "-"}, - {'.', "."}, - {'/', "/"}, - {'0', "0"}, - {'1', "1"}, - {'2', "2"}, - {'3', "3"}, - {'4', "4"}, - {'5', "5"}, - {'6', "6"}, - {'7', "7"}, - {'8', "8"}, - {'9', "9"}, - {':', ":"}, - {';', ";"}, - {'<', "<"}, - {'=', "="}, - {'>', ">"}, - {'?', "?"}, - {'@', "@"}, - {'A', "A"}, - {'B', "B"}, - {'C', "C"}, - {'D', "D"}, - {'E', "E"}, - {'F', "F"}, - {'G', "G"}, - {'H', "H"}, - {'I', "I"}, - {'J', "J"}, - {'K', "K"}, - {'L', "L"}, - {'M', "M"}, - {'N', "N"}, - {'O', "O"}, - {'P', "P"}, - {'Q', "Q"}, - {'R', "R"}, - {'S', "S"}, - {'T', "T"}, - {'U', "U"}, - {'V', "V"}, - {'W', "W"}, - {'X', "X"}, - {'Y', "Y"}, - {'Z', "Z"}, - {'[', "["}, - {'\', "\\"}, - {']', "]"}, - {'^', "^"}, - {'_', "_"}, - {'`', "`"}, - {'a', "a"}, - {'b', "b"}, - {'c', "c"}, - {'d', "d"}, - {'e', "e"}, - {'f', "f"}, - {'g', "g"}, - {'h', "h"}, - {'i', "i"}, - {'j', "j"}, - {'k', "k"}, - {'l', "l"}, - {'m', "m"}, - {'n', "n"}, - {'o', "o"}, - {'p', "p"}, - {'q', "q"}, - {'r', "r"}, - {'s', "s"}, - {'t', "t"}, - {'u', "u"}, - {'v', "v"}, - {'w', "w"}, - {'x', "x"}, - {'y', "y"}, - {'z', "z"}, - {'{', "{"}, - {'|', "|"}, - {'}', "}"}, - {'~', "~"}, - }; + return c >= 160 && CodeToAscii.TryGetValue(c, out string? str) ? str : null; } + + // This table was generated by the app UnicodeNormDApp + private static readonly Dictionary CodeToAscii = new(1269) + { + {'Ḋ', "D"}, + {'Ḍ', "D"}, + {'È', "E"}, + {'Ē', "E"}, + {'Ḕ', "E"}, + {'ª', "a"}, + {'²', "2"}, + {'³', "3"}, + {'¹', "1"}, + {'º', "o"}, + {'¼', "14"}, + {'½', "12"}, + {'¾', "34"}, + {'À', "A"}, + {'Á', "A"}, + {'Â', "A"}, + {'Ã', "A"}, + {'Ä', "A"}, + {'Å', "A"}, + {'Ç', "C"}, + {'É', "E"}, + {'Ê', "E"}, + {'Ë', "E"}, + {'Ì', "I"}, + {'Í', "I"}, + {'Î', "I"}, + {'Ï', "I"}, + {'Ñ', "N"}, + {'Ò', "O"}, + {'Ó', "O"}, + {'Ô', "O"}, + {'Õ', "O"}, + {'Ö', "O"}, + {'Ù', "U"}, + {'Ú', "U"}, + {'Û', "U"}, + {'Ü', "U"}, + {'Ý', "Y"}, + {'à', "a"}, + {'á', "a"}, + {'â', "a"}, + {'ã', "a"}, + {'ä', "a"}, + {'å', "a"}, + {'ç', "c"}, + {'è', "e"}, + {'é', "e"}, + {'ê', "e"}, + {'ë', "e"}, + {'ì', "i"}, + {'í', "i"}, + {'î', "i"}, + {'ï', "i"}, + {'ñ', "n"}, + {'ò', "o"}, + {'ó', "o"}, + {'ô', "o"}, + {'õ', "o"}, + {'ö', "o"}, + {'ù', "u"}, + {'ú', "u"}, + {'û', "u"}, + {'ü', "u"}, + {'ý', "y"}, + {'ÿ', "y"}, + {'Ā', "A"}, + {'ā', "a"}, + {'Ă', "A"}, + {'ă', "a"}, + {'Ą', "A"}, + {'ą', "a"}, + {'Ć', "C"}, + {'ć', "c"}, + {'Ĉ', "C"}, + {'ĉ', "c"}, + {'Ċ', "C"}, + {'ċ', "c"}, + {'Č', "C"}, + {'č', "c"}, + {'Ď', "D"}, + {'ď', "d"}, + {'ē', "e"}, + {'Ĕ', "E"}, + {'ĕ', "e"}, + {'Ė', "E"}, + {'ė', "e"}, + {'Ę', "E"}, + {'ę', "e"}, + {'Ě', "E"}, + {'ě', "e"}, + {'Ĝ', "G"}, + {'ĝ', "g"}, + {'Ğ', "G"}, + {'ğ', "g"}, + {'Ġ', "G"}, + {'ġ', "g"}, + {'Ģ', "G"}, + {'ģ', "g"}, + {'Ĥ', "H"}, + {'ĥ', "h"}, + {'Ĩ', "I"}, + {'ĩ', "i"}, + {'Ī', "I"}, + {'ī', "i"}, + {'Ĭ', "I"}, + {'ĭ', "i"}, + {'Į', "I"}, + {'į', "i"}, + {'İ', "I"}, + {'IJ', "IJ"}, + {'ij', "ij"}, + {'Ĵ', "J"}, + {'ĵ', "j"}, + {'Ķ', "K"}, + {'ķ', "k"}, + {'Ĺ', "L"}, + {'ĺ', "l"}, + {'Ļ', "L"}, + {'ļ', "l"}, + {'Ľ', "L"}, + {'ľ', "l"}, + {'Ŀ', "L"}, + {'ŀ', "l"}, + {'Ń', "N"}, + {'ń', "n"}, + {'Ņ', "N"}, + {'ņ', "n"}, + {'Ň', "N"}, + {'ň', "n"}, + {'ʼn', "n"}, + {'Ō', "O"}, + {'ō', "o"}, + {'Ŏ', "O"}, + {'ŏ', "o"}, + {'Ő', "O"}, + {'ő', "o"}, + {'Ŕ', "R"}, + {'ŕ', "r"}, + {'Ŗ', "R"}, + {'ŗ', "r"}, + {'Ř', "R"}, + {'ř', "r"}, + {'Ś', "S"}, + {'ś', "s"}, + {'Ŝ', "S"}, + {'ŝ', "s"}, + {'Ş', "S"}, + {'ş', "s"}, + {'Š', "S"}, + {'š', "s"}, + {'Ţ', "T"}, + {'ţ', "t"}, + {'Ť', "T"}, + {'ť', "t"}, + {'Ũ', "U"}, + {'ũ', "u"}, + {'Ū', "U"}, + {'ū', "u"}, + {'Ŭ', "U"}, + {'ŭ', "u"}, + {'Ů', "U"}, + {'ů', "u"}, + {'Ű', "U"}, + {'ű', "u"}, + {'Ų', "U"}, + {'ų', "u"}, + {'Ŵ', "W"}, + {'ŵ', "w"}, + {'Ŷ', "Y"}, + {'ŷ', "y"}, + {'Ÿ', "Y"}, + {'Ź', "Z"}, + {'ź', "z"}, + {'Ż', "Z"}, + {'ż', "z"}, + {'Ž', "Z"}, + {'ž', "z"}, + {'ſ', "s"}, + {'Ơ', "O"}, + {'ơ', "o"}, + {'Ư', "U"}, + {'ư', "u"}, + {'DŽ', "DZ"}, + {'Dž', "Dz"}, + {'dž', "dz"}, + {'LJ', "LJ"}, + {'Lj', "Lj"}, + {'lj', "lj"}, + {'NJ', "NJ"}, + {'Nj', "Nj"}, + {'nj', "nj"}, + {'Ǎ', "A"}, + {'ǎ', "a"}, + {'Ǐ', "I"}, + {'ǐ', "i"}, + {'Ǒ', "O"}, + {'ǒ', "o"}, + {'Ǔ', "U"}, + {'ǔ', "u"}, + {'Ǖ', "U"}, + {'ǖ', "u"}, + {'Ǘ', "U"}, + {'ǘ', "u"}, + {'Ǚ', "U"}, + {'ǚ', "u"}, + {'Ǜ', "U"}, + {'ǜ', "u"}, + {'Ǟ', "A"}, + {'ǟ', "a"}, + {'Ǡ', "A"}, + {'ǡ', "a"}, + {'Ǧ', "G"}, + {'ǧ', "g"}, + {'Ǩ', "K"}, + {'ǩ', "k"}, + {'Ǫ', "O"}, + {'ǫ', "o"}, + {'Ǭ', "O"}, + {'ǭ', "o"}, + {'ǰ', "j"}, + {'DZ', "DZ"}, + {'Dz', "Dz"}, + {'dz', "dz"}, + {'Ǵ', "G"}, + {'ǵ', "g"}, + {'Ǹ', "N"}, + {'ǹ', "n"}, + {'Ǻ', "A"}, + {'ǻ', "a"}, + {'Ȁ', "A"}, + {'ȁ', "a"}, + {'Ȃ', "A"}, + {'ȃ', "a"}, + {'Ȅ', "E"}, + {'ȅ', "e"}, + {'Ȇ', "E"}, + {'ȇ', "e"}, + {'Ȉ', "I"}, + {'ȉ', "i"}, + {'Ȋ', "I"}, + {'ȋ', "i"}, + {'Ȍ', "O"}, + {'ȍ', "o"}, + {'Ȏ', "O"}, + {'ȏ', "o"}, + {'Ȑ', "R"}, + {'ȑ', "r"}, + {'Ȓ', "R"}, + {'ȓ', "r"}, + {'Ȕ', "U"}, + {'ȕ', "u"}, + {'Ȗ', "U"}, + {'ȗ', "u"}, + {'Ș', "S"}, + {'ș', "s"}, + {'Ț', "T"}, + {'ț', "t"}, + {'Ȟ', "H"}, + {'ȟ', "h"}, + {'Ȧ', "A"}, + {'ȧ', "a"}, + {'Ȩ', "E"}, + {'ȩ', "e"}, + {'Ȫ', "O"}, + {'ȫ', "o"}, + {'Ȭ', "O"}, + {'ȭ', "o"}, + {'Ȯ', "O"}, + {'ȯ', "o"}, + {'Ȱ', "O"}, + {'ȱ', "o"}, + {'Ȳ', "Y"}, + {'ȳ', "y"}, + {'ʰ', "h"}, + {'ʲ', "j"}, + {'ʳ', "r"}, + {'ʷ', "w"}, + {'ʸ', "y"}, + {'ˡ', "l"}, + {'ˢ', "s"}, + {'ˣ', "x"}, + {';', ";"}, + {'ᴬ', "A"}, + {'ᴮ', "B"}, + {'ᴰ', "D"}, + {'ᴱ', "E"}, + {'ᴳ', "G"}, + {'ᴴ', "H"}, + {'ᴵ', "I"}, + {'ᴶ', "J"}, + {'ᴷ', "K"}, + {'ᴸ', "L"}, + {'ᴹ', "M"}, + {'ᴺ', "N"}, + {'ᴼ', "O"}, + {'ᴾ', "P"}, + {'ᴿ', "R"}, + {'ᵀ', "T"}, + {'ᵁ', "U"}, + {'ᵂ', "W"}, + {'ᵃ', "a"}, + {'ᵇ', "b"}, + {'ᵈ', "d"}, + {'ᵉ', "e"}, + {'ᵍ', "g"}, + {'ᵏ', "k"}, + {'ᵐ', "m"}, + {'ᵒ', "o"}, + {'ᵖ', "p"}, + {'ᵗ', "t"}, + {'ᵘ', "u"}, + {'ᵛ', "v"}, + {'ᵢ', "i"}, + {'ᵣ', "r"}, + {'ᵤ', "u"}, + {'ᵥ', "v"}, + {'ᶜ', "c"}, + {'ᶠ', "f"}, + {'ᶻ', "z"}, + {'Ḁ', "A"}, + {'ḁ', "a"}, + {'Ḃ', "B"}, + {'ḃ', "b"}, + {'Ḅ', "B"}, + {'ḅ', "b"}, + {'Ḇ', "B"}, + {'ḇ', "b"}, + {'Ḉ', "C"}, + {'ḉ', "c"}, + {'ḋ', "d"}, + {'ḍ', "d"}, + {'Ḏ', "D"}, + {'ḏ', "d"}, + {'Ḑ', "D"}, + {'ḑ', "d"}, + {'Ḓ', "D"}, + {'ḓ', "d"}, + {'ḕ', "e"}, + {'Ḗ', "E"}, + {'ḗ', "e"}, + {'Ḙ', "E"}, + {'ḙ', "e"}, + {'Ḛ', "E"}, + {'ḛ', "e"}, + {'Ḝ', "E"}, + {'ḝ', "e"}, + {'Ḟ', "F"}, + {'ḟ', "f"}, + {'Ḡ', "G"}, + {'ḡ', "g"}, + {'Ḣ', "H"}, + {'ḣ', "h"}, + {'Ḥ', "H"}, + {'ḥ', "h"}, + {'Ḧ', "H"}, + {'ḧ', "h"}, + {'Ḩ', "H"}, + {'ḩ', "h"}, + {'Ḫ', "H"}, + {'ḫ', "h"}, + {'Ḭ', "I"}, + {'ḭ', "i"}, + {'Ḯ', "I"}, + {'ḯ', "i"}, + {'Ḱ', "K"}, + {'ḱ', "k"}, + {'Ḳ', "K"}, + {'ḳ', "k"}, + {'Ḵ', "K"}, + {'ḵ', "k"}, + {'Ḷ', "L"}, + {'ḷ', "l"}, + {'Ḹ', "L"}, + {'ḹ', "l"}, + {'Ḻ', "L"}, + {'ḻ', "l"}, + {'Ḽ', "L"}, + {'ḽ', "l"}, + {'Ḿ', "M"}, + {'ḿ', "m"}, + {'Ṁ', "M"}, + {'ṁ', "m"}, + {'Ṃ', "M"}, + {'ṃ', "m"}, + {'Ṅ', "N"}, + {'ṅ', "n"}, + {'Ṇ', "N"}, + {'ṇ', "n"}, + {'Ṉ', "N"}, + {'ṉ', "n"}, + {'Ṋ', "N"}, + {'ṋ', "n"}, + {'Ṍ', "O"}, + {'ṍ', "o"}, + {'Ṏ', "O"}, + {'ṏ', "o"}, + {'Ṑ', "O"}, + {'ṑ', "o"}, + {'Ṓ', "O"}, + {'ṓ', "o"}, + {'Ṕ', "P"}, + {'ṕ', "p"}, + {'Ṗ', "P"}, + {'ṗ', "p"}, + {'Ṙ', "R"}, + {'ṙ', "r"}, + {'Ṛ', "R"}, + {'ṛ', "r"}, + {'Ṝ', "R"}, + {'ṝ', "r"}, + {'Ṟ', "R"}, + {'ṟ', "r"}, + {'Ṡ', "S"}, + {'ṡ', "s"}, + {'Ṣ', "S"}, + {'ṣ', "s"}, + {'Ṥ', "S"}, + {'ṥ', "s"}, + {'Ṧ', "S"}, + {'ṧ', "s"}, + {'Ṩ', "S"}, + {'ṩ', "s"}, + {'Ṫ', "T"}, + {'ṫ', "t"}, + {'Ṭ', "T"}, + {'ṭ', "t"}, + {'Ṯ', "T"}, + {'ṯ', "t"}, + {'Ṱ', "T"}, + {'ṱ', "t"}, + {'Ṳ', "U"}, + {'ṳ', "u"}, + {'Ṵ', "U"}, + {'ṵ', "u"}, + {'Ṷ', "U"}, + {'ṷ', "u"}, + {'Ṹ', "U"}, + {'ṹ', "u"}, + {'Ṻ', "U"}, + {'ṻ', "u"}, + {'Ṽ', "V"}, + {'ṽ', "v"}, + {'Ṿ', "V"}, + {'ṿ', "v"}, + {'Ẁ', "W"}, + {'ẁ', "w"}, + {'Ẃ', "W"}, + {'ẃ', "w"}, + {'Ẅ', "W"}, + {'ẅ', "w"}, + {'Ẇ', "W"}, + {'ẇ', "w"}, + {'Ẉ', "W"}, + {'ẉ', "w"}, + {'Ẋ', "X"}, + {'ẋ', "x"}, + {'Ẍ', "X"}, + {'ẍ', "x"}, + {'Ẏ', "Y"}, + {'ẏ', "y"}, + {'Ẑ', "Z"}, + {'ẑ', "z"}, + {'Ẓ', "Z"}, + {'ẓ', "z"}, + {'Ẕ', "Z"}, + {'ẕ', "z"}, + {'ẖ', "h"}, + {'ẗ', "t"}, + {'ẘ', "w"}, + {'ẙ', "y"}, + {'ẚ', "a"}, + {'ẛ', "s"}, + {'Ạ', "A"}, + {'ạ', "a"}, + {'Ả', "A"}, + {'ả', "a"}, + {'Ấ', "A"}, + {'ấ', "a"}, + {'Ầ', "A"}, + {'ầ', "a"}, + {'Ẩ', "A"}, + {'ẩ', "a"}, + {'Ẫ', "A"}, + {'ẫ', "a"}, + {'Ậ', "A"}, + {'ậ', "a"}, + {'Ắ', "A"}, + {'ắ', "a"}, + {'Ằ', "A"}, + {'ằ', "a"}, + {'Ẳ', "A"}, + {'ẳ', "a"}, + {'Ẵ', "A"}, + {'ẵ', "a"}, + {'Ặ', "A"}, + {'ặ', "a"}, + {'Ẹ', "E"}, + {'ẹ', "e"}, + {'Ẻ', "E"}, + {'ẻ', "e"}, + {'Ẽ', "E"}, + {'ẽ', "e"}, + {'Ế', "E"}, + {'ế', "e"}, + {'Ề', "E"}, + {'ề', "e"}, + {'Ể', "E"}, + {'ể', "e"}, + {'Ễ', "E"}, + {'ễ', "e"}, + {'Ệ', "E"}, + {'ệ', "e"}, + {'Ỉ', "I"}, + {'ỉ', "i"}, + {'Ị', "I"}, + {'ị', "i"}, + {'Ọ', "O"}, + {'ọ', "o"}, + {'Ỏ', "O"}, + {'ỏ', "o"}, + {'Ố', "O"}, + {'ố', "o"}, + {'Ồ', "O"}, + {'ồ', "o"}, + {'Ổ', "O"}, + {'ổ', "o"}, + {'Ỗ', "O"}, + {'ỗ', "o"}, + {'Ộ', "O"}, + {'ộ', "o"}, + {'Ớ', "O"}, + {'ớ', "o"}, + {'Ờ', "O"}, + {'ờ', "o"}, + {'Ở', "O"}, + {'ở', "o"}, + {'Ỡ', "O"}, + {'ỡ', "o"}, + {'Ợ', "O"}, + {'ợ', "o"}, + {'Ụ', "U"}, + {'ụ', "u"}, + {'Ủ', "U"}, + {'ủ', "u"}, + {'Ứ', "U"}, + {'ứ', "u"}, + {'Ừ', "U"}, + {'ừ', "u"}, + {'Ử', "U"}, + {'ử', "u"}, + {'Ữ', "U"}, + {'ữ', "u"}, + {'Ự', "U"}, + {'ự', "u"}, + {'Ỳ', "Y"}, + {'ỳ', "y"}, + {'Ỵ', "Y"}, + {'ỵ', "y"}, + {'Ỷ', "Y"}, + {'ỷ', "y"}, + {'Ỹ', "Y"}, + {'ỹ', "y"}, + {'`', "`"}, + {'․', "."}, + {'‥', ".."}, + {'…', "..."}, + {'‼', "!!"}, + {'⁇', "??"}, + {'⁈', "?!"}, + {'⁉', "!?"}, + {'⁰', "0"}, + {'ⁱ', "i"}, + {'⁴', "4"}, + {'⁵', "5"}, + {'⁶', "6"}, + {'⁷', "7"}, + {'⁸', "8"}, + {'⁹', "9"}, + {'⁺', "+"}, + {'⁼', "="}, + {'⁽', "("}, + {'⁾', ")"}, + {'ⁿ', "n"}, + {'₀', "0"}, + {'₁', "1"}, + {'₂', "2"}, + {'₃', "3"}, + {'₄', "4"}, + {'₅', "5"}, + {'₆', "6"}, + {'₇', "7"}, + {'₈', "8"}, + {'₉', "9"}, + {'₊', "+"}, + {'₌', "="}, + {'₍', "("}, + {'₎', ")"}, + {'ₐ', "a"}, + {'ₑ', "e"}, + {'ₒ', "o"}, + {'ₓ', "x"}, + {'ₕ', "h"}, + {'ₖ', "k"}, + {'ₗ', "l"}, + {'ₘ', "m"}, + {'ₙ', "n"}, + {'ₚ', "p"}, + {'ₛ', "s"}, + {'ₜ', "t"}, + {'₨', "Rs"}, + {'℀', "a/c"}, + {'℁', "a/s"}, + {'ℂ', "C"}, + {'℃', "C"}, + {'℅', "c/o"}, + {'℆', "c/u"}, + {'℉', "F"}, + {'ℊ', "g"}, + {'ℋ', "H"}, + {'ℌ', "H"}, + {'ℍ', "H"}, + {'ℎ', "h"}, + {'ℐ', "I"}, + {'ℑ', "I"}, + {'ℒ', "L"}, + {'ℓ', "l"}, + {'ℕ', "N"}, + {'№', "No"}, + {'ℙ', "P"}, + {'ℚ', "Q"}, + {'ℛ', "R"}, + {'ℜ', "R"}, + {'ℝ', "R"}, + {'℠', "SM"}, + {'℡', "TEL"}, + {'™', "TM"}, + {'ℤ', "Z"}, + {'ℨ', "Z"}, + {'K', "K"}, + {'Å', "A"}, + {'ℬ', "B"}, + {'ℭ', "C"}, + {'ℯ', "e"}, + {'ℰ', "E"}, + {'ℱ', "F"}, + {'ℳ', "M"}, + {'ℴ', "o"}, + {'ℹ', "i"}, + {'℻', "FAX"}, + {'ⅅ', "D"}, + {'ⅆ', "d"}, + {'ⅇ', "e"}, + {'ⅈ', "i"}, + {'ⅉ', "j"}, + {'⅐', "17"}, + {'⅑', "19"}, + {'⅒', "110"}, + {'⅓', "13"}, + {'⅔', "23"}, + {'⅕', "15"}, + {'⅖', "25"}, + {'⅗', "35"}, + {'⅘', "45"}, + {'⅙', "16"}, + {'⅚', "56"}, + {'⅛', "18"}, + {'⅜', "38"}, + {'⅝', "58"}, + {'⅞', "78"}, + {'⅟', "1"}, + {'Ⅰ', "I"}, + {'Ⅱ', "II"}, + {'Ⅲ', "III"}, + {'Ⅳ', "IV"}, + {'Ⅴ', "V"}, + {'Ⅵ', "VI"}, + {'Ⅶ', "VII"}, + {'Ⅷ', "VIII"}, + {'Ⅸ', "IX"}, + {'Ⅹ', "X"}, + {'Ⅺ', "XI"}, + {'Ⅻ', "XII"}, + {'Ⅼ', "L"}, + {'Ⅽ', "C"}, + {'Ⅾ', "D"}, + {'Ⅿ', "M"}, + {'ⅰ', "i"}, + {'ⅱ', "ii"}, + {'ⅲ', "iii"}, + {'ⅳ', "iv"}, + {'ⅴ', "v"}, + {'ⅵ', "vi"}, + {'ⅶ', "vii"}, + {'ⅷ', "viii"}, + {'ⅸ', "ix"}, + {'ⅹ', "x"}, + {'ⅺ', "xi"}, + {'ⅻ', "xii"}, + {'ⅼ', "l"}, + {'ⅽ', "c"}, + {'ⅾ', "d"}, + {'ⅿ', "m"}, + {'↉', "03"}, + {'≠', "="}, + {'≮', "<"}, + {'≯', ">"}, + {'①', "1"}, + {'②', "2"}, + {'③', "3"}, + {'④', "4"}, + {'⑤', "5"}, + {'⑥', "6"}, + {'⑦', "7"}, + {'⑧', "8"}, + {'⑨', "9"}, + {'⑩', "10"}, + {'⑪', "11"}, + {'⑫', "12"}, + {'⑬', "13"}, + {'⑭', "14"}, + {'⑮', "15"}, + {'⑯', "16"}, + {'⑰', "17"}, + {'⑱', "18"}, + {'⑲', "19"}, + {'⑳', "20"}, + {'⑴', "(1)"}, + {'⑵', "(2)"}, + {'⑶', "(3)"}, + {'⑷', "(4)"}, + {'⑸', "(5)"}, + {'⑹', "(6)"}, + {'⑺', "(7)"}, + {'⑻', "(8)"}, + {'⑼', "(9)"}, + {'⑽', "(10)"}, + {'⑾', "(11)"}, + {'⑿', "(12)"}, + {'⒀', "(13)"}, + {'⒁', "(14)"}, + {'⒂', "(15)"}, + {'⒃', "(16)"}, + {'⒄', "(17)"}, + {'⒅', "(18)"}, + {'⒆', "(19)"}, + {'⒇', "(20)"}, + {'⒈', "1."}, + {'⒉', "2."}, + {'⒊', "3."}, + {'⒋', "4."}, + {'⒌', "5."}, + {'⒍', "6."}, + {'⒎', "7."}, + {'⒏', "8."}, + {'⒐', "9."}, + {'⒑', "10."}, + {'⒒', "11."}, + {'⒓', "12."}, + {'⒔', "13."}, + {'⒕', "14."}, + {'⒖', "15."}, + {'⒗', "16."}, + {'⒘', "17."}, + {'⒙', "18."}, + {'⒚', "19."}, + {'⒛', "20."}, + {'⒜', "(a)"}, + {'⒝', "(b)"}, + {'⒞', "(c)"}, + {'⒟', "(d)"}, + {'⒠', "(e)"}, + {'⒡', "(f)"}, + {'⒢', "(g)"}, + {'⒣', "(h)"}, + {'⒤', "(i)"}, + {'⒥', "(j)"}, + {'⒦', "(k)"}, + {'⒧', "(l)"}, + {'⒨', "(m)"}, + {'⒩', "(n)"}, + {'⒪', "(o)"}, + {'⒫', "(p)"}, + {'⒬', "(q)"}, + {'⒭', "(r)"}, + {'⒮', "(s)"}, + {'⒯', "(t)"}, + {'⒰', "(u)"}, + {'⒱', "(v)"}, + {'⒲', "(w)"}, + {'⒳', "(x)"}, + {'⒴', "(y)"}, + {'⒵', "(z)"}, + {'Ⓐ', "A"}, + {'Ⓑ', "B"}, + {'Ⓒ', "C"}, + {'Ⓓ', "D"}, + {'Ⓔ', "E"}, + {'Ⓕ', "F"}, + {'Ⓖ', "G"}, + {'Ⓗ', "H"}, + {'Ⓘ', "I"}, + {'Ⓙ', "J"}, + {'Ⓚ', "K"}, + {'Ⓛ', "L"}, + {'Ⓜ', "M"}, + {'Ⓝ', "N"}, + {'Ⓞ', "O"}, + {'Ⓟ', "P"}, + {'Ⓠ', "Q"}, + {'Ⓡ', "R"}, + {'Ⓢ', "S"}, + {'Ⓣ', "T"}, + {'Ⓤ', "U"}, + {'Ⓥ', "V"}, + {'Ⓦ', "W"}, + {'Ⓧ', "X"}, + {'Ⓨ', "Y"}, + {'Ⓩ', "Z"}, + {'ⓐ', "a"}, + {'ⓑ', "b"}, + {'ⓒ', "c"}, + {'ⓓ', "d"}, + {'ⓔ', "e"}, + {'ⓕ', "f"}, + {'ⓖ', "g"}, + {'ⓗ', "h"}, + {'ⓘ', "i"}, + {'ⓙ', "j"}, + {'ⓚ', "k"}, + {'ⓛ', "l"}, + {'ⓜ', "m"}, + {'ⓝ', "n"}, + {'ⓞ', "o"}, + {'ⓟ', "p"}, + {'ⓠ', "q"}, + {'ⓡ', "r"}, + {'ⓢ', "s"}, + {'ⓣ', "t"}, + {'ⓤ', "u"}, + {'ⓥ', "v"}, + {'ⓦ', "w"}, + {'ⓧ', "x"}, + {'ⓨ', "y"}, + {'ⓩ', "z"}, + {'⓪', "0"}, + {'⩴', "::="}, + {'⩵', "=="}, + {'⩶', "==="}, + {'ⱼ', "j"}, + {'ⱽ', "V"}, + {'㈀', "()"}, + {'㈁', "()"}, + {'㈂', "()"}, + {'㈃', "()"}, + {'㈄', "()"}, + {'㈅', "()"}, + {'㈆', "()"}, + {'㈇', "()"}, + {'㈈', "()"}, + {'㈉', "()"}, + {'㈊', "()"}, + {'㈋', "()"}, + {'㈌', "()"}, + {'㈍', "()"}, + {'㈎', "()"}, + {'㈏', "()"}, + {'㈐', "()"}, + {'㈑', "()"}, + {'㈒', "()"}, + {'㈓', "()"}, + {'㈔', "()"}, + {'㈕', "()"}, + {'㈖', "()"}, + {'㈗', "()"}, + {'㈘', "()"}, + {'㈙', "()"}, + {'㈚', "()"}, + {'㈛', "()"}, + {'㈜', "()"}, + {'㈝', "()"}, + {'㈞', "()"}, + {'㈠', "()"}, + {'㈡', "()"}, + {'㈢', "()"}, + {'㈣', "()"}, + {'㈤', "()"}, + {'㈥', "()"}, + {'㈦', "()"}, + {'㈧', "()"}, + {'㈨', "()"}, + {'㈩', "()"}, + {'㈪', "()"}, + {'㈫', "()"}, + {'㈬', "()"}, + {'㈭', "()"}, + {'㈮', "()"}, + {'㈯', "()"}, + {'㈰', "()"}, + {'㈱', "()"}, + {'㈲', "()"}, + {'㈳', "()"}, + {'㈴', "()"}, + {'㈵', "()"}, + {'㈶', "()"}, + {'㈷', "()"}, + {'㈸', "()"}, + {'㈹', "()"}, + {'㈺', "()"}, + {'㈻', "()"}, + {'㈼', "()"}, + {'㈽', "()"}, + {'㈾', "()"}, + {'㈿', "()"}, + {'㉀', "()"}, + {'㉁', "()"}, + {'㉂', "()"}, + {'㉃', "()"}, + {'㉐', "PTE"}, + {'㉑', "21"}, + {'㉒', "22"}, + {'㉓', "23"}, + {'㉔', "24"}, + {'㉕', "25"}, + {'㉖', "26"}, + {'㉗', "27"}, + {'㉘', "28"}, + {'㉙', "29"}, + {'㉚', "30"}, + {'㉛', "31"}, + {'㉜', "32"}, + {'㉝', "33"}, + {'㉞', "34"}, + {'㉟', "35"}, + {'㊱', "36"}, + {'㊲', "37"}, + {'㊳', "38"}, + {'㊴', "39"}, + {'㊵', "40"}, + {'㊶', "41"}, + {'㊷', "42"}, + {'㊸', "43"}, + {'㊹', "44"}, + {'㊺', "45"}, + {'㊻', "46"}, + {'㊼', "47"}, + {'㊽', "48"}, + {'㊾', "49"}, + {'㊿', "50"}, + {'㋀', "1"}, + {'㋁', "2"}, + {'㋂', "3"}, + {'㋃', "4"}, + {'㋄', "5"}, + {'㋅', "6"}, + {'㋆', "7"}, + {'㋇', "8"}, + {'㋈', "9"}, + {'㋉', "10"}, + {'㋊', "11"}, + {'㋋', "12"}, + {'㋌', "Hg"}, + {'㋍', "erg"}, + {'㋎', "eV"}, + {'㋏', "LTD"}, + {'㍘', "0"}, + {'㍙', "1"}, + {'㍚', "2"}, + {'㍛', "3"}, + {'㍜', "4"}, + {'㍝', "5"}, + {'㍞', "6"}, + {'㍟', "7"}, + {'㍠', "8"}, + {'㍡', "9"}, + {'㍢', "10"}, + {'㍣', "11"}, + {'㍤', "12"}, + {'㍥', "13"}, + {'㍦', "14"}, + {'㍧', "15"}, + {'㍨', "16"}, + {'㍩', "17"}, + {'㍪', "18"}, + {'㍫', "19"}, + {'㍬', "20"}, + {'㍭', "21"}, + {'㍮', "22"}, + {'㍯', "23"}, + {'㍰', "24"}, + {'㍱', "hPa"}, + {'㍲', "da"}, + {'㍳', "AU"}, + {'㍴', "bar"}, + {'㍵', "oV"}, + {'㍶', "pc"}, + {'㍷', "dm"}, + {'㍸', "dm2"}, + {'㍹', "dm3"}, + {'㍺', "IU"}, + {'㎀', "pA"}, + {'㎁', "nA"}, + {'㎂', "A"}, + {'㎃', "mA"}, + {'㎄', "kA"}, + {'㎅', "KB"}, + {'㎆', "MB"}, + {'㎇', "GB"}, + {'㎈', "cal"}, + {'㎉', "kcal"}, + {'㎊', "pF"}, + {'㎋', "nF"}, + {'㎌', "F"}, + {'㎍', "g"}, + {'㎎', "mg"}, + {'㎏', "kg"}, + {'㎐', "Hz"}, + {'㎑', "kHz"}, + {'㎒', "MHz"}, + {'㎓', "GHz"}, + {'㎔', "THz"}, + {'㎕', "l"}, + {'㎖', "ml"}, + {'㎗', "dl"}, + {'㎘', "kl"}, + {'㎙', "fm"}, + {'㎚', "nm"}, + {'㎛', "m"}, + {'㎜', "mm"}, + {'㎝', "cm"}, + {'㎞', "km"}, + {'㎟', "mm2"}, + {'㎠', "cm2"}, + {'㎡', "m2"}, + {'㎢', "km2"}, + {'㎣', "mm3"}, + {'㎤', "cm3"}, + {'㎥', "m3"}, + {'㎦', "km3"}, + {'㎧', "ms"}, + {'㎨', "ms2"}, + {'㎩', "Pa"}, + {'㎪', "kPa"}, + {'㎫', "MPa"}, + {'㎬', "GPa"}, + {'㎭', "rad"}, + {'㎮', "rads"}, + {'㎯', "rads2"}, + {'㎰', "ps"}, + {'㎱', "ns"}, + {'㎲', "s"}, + {'㎳', "ms"}, + {'㎴', "pV"}, + {'㎵', "nV"}, + {'㎶', "V"}, + {'㎷', "mV"}, + {'㎸', "kV"}, + {'㎹', "MV"}, + {'㎺', "pW"}, + {'㎻', "nW"}, + {'㎼', "W"}, + {'㎽', "mW"}, + {'㎾', "kW"}, + {'㎿', "MW"}, + {'㏀', "k"}, + {'㏁', "M"}, + {'㏂', "a.m."}, + {'㏃', "Bq"}, + {'㏄', "cc"}, + {'㏅', "cd"}, + {'㏆', "Ckg"}, + {'㏇', "Co."}, + {'㏈', "dB"}, + {'㏉', "Gy"}, + {'㏊', "ha"}, + {'㏋', "HP"}, + {'㏌', "in"}, + {'㏍', "KK"}, + {'㏎', "KM"}, + {'㏏', "kt"}, + {'㏐', "lm"}, + {'㏑', "ln"}, + {'㏒', "log"}, + {'㏓', "lx"}, + {'㏔', "mb"}, + {'㏕', "mil"}, + {'㏖', "mol"}, + {'㏗', "PH"}, + {'㏘', "p.m."}, + {'㏙', "PPM"}, + {'㏚', "PR"}, + {'㏛', "sr"}, + {'㏜', "Sv"}, + {'㏝', "Wb"}, + {'㏞', "Vm"}, + {'㏟', "Am"}, + {'㏠', "1"}, + {'㏡', "2"}, + {'㏢', "3"}, + {'㏣', "4"}, + {'㏤', "5"}, + {'㏥', "6"}, + {'㏦', "7"}, + {'㏧', "8"}, + {'㏨', "9"}, + {'㏩', "10"}, + {'㏪', "11"}, + {'㏫', "12"}, + {'㏬', "13"}, + {'㏭', "14"}, + {'㏮', "15"}, + {'㏯', "16"}, + {'㏰', "17"}, + {'㏱', "18"}, + {'㏲', "19"}, + {'㏳', "20"}, + {'㏴', "21"}, + {'㏵', "22"}, + {'㏶', "23"}, + {'㏷', "24"}, + {'㏸', "25"}, + {'㏹', "26"}, + {'㏺', "27"}, + {'㏻', "28"}, + {'㏼', "29"}, + {'㏽', "30"}, + {'㏾', "31"}, + {'㏿', "gal"}, + {'ff', "ff"}, + {'fi', "fi"}, + {'fl', "fl"}, + {'ffi', "ffi"}, + {'ffl', "ffl"}, + {'ſt', "st"}, + {'st', "st"}, + {'﬩', "+"}, + {'︐', ","}, + {'︓', ":"}, + {'︔', ";"}, + {'︕', "!"}, + {'︖', "?"}, + {'︙', "..."}, + {'︰', ".."}, + {'︳', "_"}, + {'︴', "_"}, + {'︵', "("}, + {'︶', ")"}, + {'︷', "{"}, + {'︸', "}"}, + {'﹇', "["}, + {'﹈', "]"}, + {'﹍', "_"}, + {'﹎', "_"}, + {'﹏', "_"}, + {'﹐', ","}, + {'﹒', "."}, + {'﹔', ";"}, + {'﹕', ":"}, + {'﹖', "?"}, + {'﹗', "!"}, + {'﹙', "("}, + {'﹚', ")"}, + {'﹛', "{"}, + {'﹜', "}"}, + {'﹟', "#"}, + {'﹠', "&"}, + {'﹡', "*"}, + {'﹢', "+"}, + {'﹣', "-"}, + {'﹤', "<"}, + {'﹥', ">"}, + {'﹦', "="}, + {'﹨', "\\"}, + {'﹩', "$"}, + {'﹪', "%"}, + {'﹫', "@"}, + {'!', "!"}, + {'"', "\""}, + {'#', "#"}, + {'$', "$"}, + {'%', "%"}, + {'&', "&"}, + {''', "'"}, + {'(', "("}, + {')', ")"}, + {'*', "*"}, + {'+', "+"}, + {',', ","}, + {'-', "-"}, + {'.', "."}, + {'/', "/"}, + {'0', "0"}, + {'1', "1"}, + {'2', "2"}, + {'3', "3"}, + {'4', "4"}, + {'5', "5"}, + {'6', "6"}, + {'7', "7"}, + {'8', "8"}, + {'9', "9"}, + {':', ":"}, + {';', ";"}, + {'<', "<"}, + {'=', "="}, + {'>', ">"}, + {'?', "?"}, + {'@', "@"}, + {'A', "A"}, + {'B', "B"}, + {'C', "C"}, + {'D', "D"}, + {'E', "E"}, + {'F', "F"}, + {'G', "G"}, + {'H', "H"}, + {'I', "I"}, + {'J', "J"}, + {'K', "K"}, + {'L', "L"}, + {'M', "M"}, + {'N', "N"}, + {'O', "O"}, + {'P', "P"}, + {'Q', "Q"}, + {'R', "R"}, + {'S', "S"}, + {'T', "T"}, + {'U', "U"}, + {'V', "V"}, + {'W', "W"}, + {'X', "X"}, + {'Y', "Y"}, + {'Z', "Z"}, + {'[', "["}, + {'\', "\\"}, + {']', "]"}, + {'^', "^"}, + {'_', "_"}, + {'`', "`"}, + {'a', "a"}, + {'b', "b"}, + {'c', "c"}, + {'d', "d"}, + {'e', "e"}, + {'f', "f"}, + {'g', "g"}, + {'h', "h"}, + {'i', "i"}, + {'j', "j"}, + {'k', "k"}, + {'l', "l"}, + {'m', "m"}, + {'n', "n"}, + {'o', "o"}, + {'p', "p"}, + {'q', "q"}, + {'r', "r"}, + {'s', "s"}, + {'t', "t"}, + {'u', "u"}, + {'v', "v"}, + {'w', "w"}, + {'x', "x"}, + {'y', "y"}, + {'z', "z"}, + {'{', "{"}, + {'|', "|"}, + {'}', "}"}, + {'~', "~"}, + }; } \ No newline at end of file diff --git a/src/Markdig/Helpers/CharacterMap.cs b/src/Markdig/Helpers/CharacterMap.cs index 067ac263e..2835131b3 100644 --- a/src/Markdig/Helpers/CharacterMap.cs +++ b/src/Markdig/Helpers/CharacterMap.cs @@ -2,8 +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; -using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; @@ -13,241 +11,205 @@ using System.Runtime.Intrinsics.X86; #endif -namespace Markdig.Helpers +namespace Markdig.Helpers; + +/// +/// Allows to associate characters to a data structures and query efficiently for them. +/// +/// +public sealed class CharacterMap where T : class { - /// - /// Allows to associate characters to a data structures and query efficiently for them. - /// - /// - public sealed class CharacterMap where T : class - { #if NETCOREAPP3_1_OR_GREATER - private readonly Vector128 _asciiBitmap; + private readonly Vector128 _asciiBitmap; #endif - private readonly T[] asciiMap; - private readonly Dictionary? nonAsciiMap; - private readonly BoolVector128 isOpeningCharacter; + private readonly T[] asciiMap; + private readonly Dictionary? nonAsciiMap; + private readonly BoolVector128 isOpeningCharacter; + + /// + /// Initializes a new instance of the class. + /// + /// The states. + /// + public CharacterMap(IEnumerable> maps) + { + if (maps is null) ThrowHelper.ArgumentNullException(nameof(maps)); + var charSet = new HashSet(); + int maxChar = 0; - /// - /// Initializes a new instance of the class. - /// - /// The states. - /// - public CharacterMap(IEnumerable> maps) + foreach (var map in maps) { - if (maps is null) ThrowHelper.ArgumentNullException(nameof(maps)); - var charSet = new HashSet(); - int maxChar = 0; + var openingChar = map.Key; + charSet.Add(openingChar); - foreach (var map in maps) + if (openingChar < 128) { - var openingChar = map.Key; - charSet.Add(openingChar); + maxChar = Math.Max(maxChar, openingChar); - if (openingChar < 128) + if (openingChar == 0) { - maxChar = Math.Max(maxChar, openingChar); - - if (openingChar == 0) - { - ThrowHelper.ArgumentOutOfRangeException("Null is not a valid opening character.", nameof(maps)); - } - } - else - { - nonAsciiMap ??= new Dictionary(); + ThrowHelper.ArgumentOutOfRangeException("Null is not a valid opening character.", nameof(maps)); } } + else + { + nonAsciiMap ??= new Dictionary(); + } + } - OpeningCharacters = charSet.ToArray(); - Array.Sort(OpeningCharacters); + OpeningCharacters = charSet.ToArray(); + Array.Sort(OpeningCharacters); - asciiMap = new T[maxChar + 1]; + asciiMap = new T[maxChar + 1]; - foreach (var state in maps) + foreach (var state in maps) + { + char openingChar = state.Key; + if (openingChar < 128) { - char openingChar = state.Key; - if (openingChar < 128) - { - asciiMap[openingChar] ??= state.Value; - isOpeningCharacter.Set(openingChar); - } - else if (!nonAsciiMap!.ContainsKey(openingChar)) - { - nonAsciiMap[openingChar] = state.Value; - } + asciiMap[openingChar] ??= state.Value; + isOpeningCharacter.Set(openingChar); } - -#if NETCOREAPP3_1_OR_GREATER - if (nonAsciiMap is null) + else if (!nonAsciiMap!.ContainsKey(openingChar)) { - long bitmap_0_3 = 0; - long bitmap_4_7 = 0; + nonAsciiMap[openingChar] = state.Value; + } + } - foreach (char openingChar in OpeningCharacters) - { - int position = (openingChar >> 4) | ((openingChar & 0x0F) << 3); - if (position < 64) bitmap_0_3 |= 1L << position; - else bitmap_4_7 |= 1L << (position - 64); - } +#if NETCOREAPP3_1_OR_GREATER + if (nonAsciiMap is null) + { + long bitmap_0_3 = 0; + long bitmap_4_7 = 0; - _asciiBitmap = Vector128.Create(bitmap_0_3, bitmap_4_7).AsByte(); + foreach (char openingChar in OpeningCharacters) + { + int position = (openingChar >> 4) | ((openingChar & 0x0F) << 3); + if (position < 64) bitmap_0_3 |= 1L << position; + else bitmap_4_7 |= 1L << (position - 64); } -#endif + + _asciiBitmap = Vector128.Create(bitmap_0_3, bitmap_4_7).AsByte(); } +#endif + } + + /// + /// Gets all the opening characters defined. + /// + public char[] OpeningCharacters { get; } - /// - /// Gets all the opening characters defined. - /// - public char[] OpeningCharacters { get; } - - /// - /// Gets the list of parsers valid for the specified opening character. - /// - /// The opening character. - /// A list of parsers valid for the specified opening character or null if no parsers registered. - public T? this[uint openingChar] + /// + /// Gets the list of parsers valid for the specified opening character. + /// + /// The opening character. + /// A list of parsers valid for the specified opening character or null if no parsers registered. + public T? this[uint openingChar] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get + T[] asciiMap = this.asciiMap; + if (openingChar < (uint)asciiMap.Length) { - T[] asciiMap = this.asciiMap; - if (openingChar < (uint)asciiMap.Length) - { - return asciiMap[openingChar]; - } - else - { - T? map = null; - nonAsciiMap?.TryGetValue(openingChar, out map); - return map; - } + return asciiMap[openingChar]; + } + else + { + T? map = null; + nonAsciiMap?.TryGetValue(openingChar, out map); + return map; } } + } - /// - /// Searches for an opening character from a registered parser in the specified string. - /// - /// The text. - /// The start. - /// The end. - /// Index position within the string of the first opening character found in the specified text; if not found, returns -1 - public int IndexOfOpeningCharacter(string text, int start, int end) - { - Debug.Assert(text is not null); - Debug.Assert(start >= 0 && end >= 0); - Debug.Assert(end - start + 1 >= 0); - Debug.Assert(end - start + 1 <= text.Length); + /// + /// Searches for an opening character from a registered parser in the specified string. + /// + /// The text. + /// The start. + /// The end. + /// Index position within the string of the first opening character found in the specified text; if not found, returns -1 + public int IndexOfOpeningCharacter(string text, int start, int end) + { + Debug.Assert(text is not null); + Debug.Assert(start >= 0 && end >= 0); + Debug.Assert(end - start + 1 >= 0); + Debug.Assert(end - start + 1 <= text.Length); - if (nonAsciiMap is null) - { + if (nonAsciiMap is null) + { #if NETCOREAPP3_1_OR_GREATER - if (Ssse3.IsSupported && BitConverter.IsLittleEndian) - { - // Based on http://0x80.pl/articles/simd-byte-lookup.html#universal-algorithm - // Optimized for sets in the [1, 127] range + if (Ssse3.IsSupported && BitConverter.IsLittleEndian) + { + // Based on http://0x80.pl/articles/simd-byte-lookup.html#universal-algorithm + // Optimized for sets in the [1, 127] range - int lengthMinusOne = end - start; - int charsToProcessVectorized = lengthMinusOne & ~(2 * Vector128.Count - 1); - int finalStart = start + charsToProcessVectorized; + int lengthMinusOne = end - start; + int charsToProcessVectorized = lengthMinusOne & ~(2 * Vector128.Count - 1); + int finalStart = start + charsToProcessVectorized; - if (start < finalStart) + if (start < finalStart) + { + ref char textStartRef = ref Unsafe.Add(ref Unsafe.AsRef(in text.GetPinnableReference()), start); + Vector128 bitmap = _asciiBitmap; + do { - ref char textStartRef = ref Unsafe.Add(ref Unsafe.AsRef(in text.GetPinnableReference()), start); - Vector128 bitmap = _asciiBitmap; - do + // Load 32 bytes (16 chars) into two Vector128s (chars) + // Drop the high byte of each char + // Pack the remaining bytes into a single Vector128 + Vector128 input = Sse2.PackUnsignedSaturate( + Unsafe.ReadUnaligned>(ref Unsafe.As(ref textStartRef)), + Unsafe.ReadUnaligned>(ref Unsafe.As(ref Unsafe.Add(ref textStartRef, Vector128.Count)))); + + // Extract the higher nibble of each character ((input >> 4) & 0xF) + Vector128 higherNibbles = Sse2.And(Sse2.ShiftRightLogical(input.AsUInt16(), 4).AsByte(), Vector128.Create((byte)0xF)); + + // Lookup the matching higher nibble for each character based on the lower nibble + // PSHUFB will set the result to 0 for any non-ASCII (> 127) character + Vector128 bitsets = Ssse3.Shuffle(bitmap, input); + + // Calculate a bitmask (1 << (higherNibble % 8)) for each character + Vector128 bitmask = Ssse3.Shuffle(Vector128.Create(0x8040201008040201).AsByte(), higherNibbles); + + // Check which characters are present in the set + // We are relying on bitsets being zero for non-ASCII characters + Vector128 result = Sse2.And(bitsets, bitmask); + + if (!result.Equals(Vector128.Zero)) { - // Load 32 bytes (16 chars) into two Vector128s (chars) - // Drop the high byte of each char - // Pack the remaining bytes into a single Vector128 - Vector128 input = Sse2.PackUnsignedSaturate( - Unsafe.ReadUnaligned>(ref Unsafe.As(ref textStartRef)), - Unsafe.ReadUnaligned>(ref Unsafe.As(ref Unsafe.Add(ref textStartRef, Vector128.Count)))); - - // Extract the higher nibble of each character ((input >> 4) & 0xF) - Vector128 higherNibbles = Sse2.And(Sse2.ShiftRightLogical(input.AsUInt16(), 4).AsByte(), Vector128.Create((byte)0xF)); - - // Lookup the matching higher nibble for each character based on the lower nibble - // PSHUFB will set the result to 0 for any non-ASCII (> 127) character - Vector128 bitsets = Ssse3.Shuffle(bitmap, input); - - // Calculate a bitmask (1 << (higherNibble % 8)) for each character - Vector128 bitmask = Ssse3.Shuffle(Vector128.Create(0x8040201008040201).AsByte(), higherNibbles); - - // Check which characters are present in the set - // We are relying on bitsets being zero for non-ASCII characters - Vector128 result = Sse2.And(bitsets, bitmask); - - if (!result.Equals(Vector128.Zero)) - { - int resultMask = ~Sse2.MoveMask(Sse2.CompareEqual(result, Vector128.Zero)); - return start + BitOperations.TrailingZeroCount((uint)resultMask); - } - - start += 2 * Vector128.Count; - textStartRef = ref Unsafe.Add(ref textStartRef, 2 * Vector128.Count); + int resultMask = ~Sse2.MoveMask(Sse2.CompareEqual(result, Vector128.Zero)); + return start + BitOperations.TrailingZeroCount((uint)resultMask); } - while (start != finalStart); + + start += 2 * Vector128.Count; + textStartRef = ref Unsafe.Add(ref textStartRef, 2 * Vector128.Count); } + while (start != finalStart); } + } - ref char textRef = ref Unsafe.AsRef(in text.GetPinnableReference()); - for (; start <= end; start++) + ref char textRef = ref Unsafe.AsRef(in text.GetPinnableReference()); + for (; start <= end; start++) + { + if (IntPtr.Size == 4) { - if (IntPtr.Size == 4) - { - uint c = Unsafe.Add(ref textRef, start); - if (c < 128 && isOpeningCharacter[c]) - { - return start; - } - } - else + uint c = Unsafe.Add(ref textRef, start); + if (c < 128 && isOpeningCharacter[c]) { - ulong c = Unsafe.Add(ref textRef, start); - if (c < 128 && isOpeningCharacter[c]) - { - return start; - } + return start; } } -#else - unsafe + else { - fixed (char* pText = text) + ulong c = Unsafe.Add(ref textRef, start); + if (c < 128 && isOpeningCharacter[c]) { - for (int i = start; i <= end; i++) - { - char c = pText[i]; - if (c < 128 && isOpeningCharacter[c]) - { - return i; - } - } + return start; } } -#endif - return -1; - } - else - { - return IndexOfOpeningCharacterNonAscii(text, start, end); - } - } - - private int IndexOfOpeningCharacterNonAscii(string text, int start, int end) - { -#if NETCOREAPP3_1_OR_GREATER - ref char textRef = ref Unsafe.AsRef(in text.GetPinnableReference()); - for (int i = start; i <= end; i++) - { - char c = Unsafe.Add(ref textRef, i); - if (c < 128 ? isOpeningCharacter[c] : nonAsciiMap!.ContainsKey(c)) - { - return i; - } } #else unsafe @@ -257,7 +219,7 @@ private int IndexOfOpeningCharacterNonAscii(string text, int start, int end) for (int i = start; i <= end; i++) { char c = pText[i]; - if (c < 128 ? isOpeningCharacter[c] : nonAsciiMap!.ContainsKey(c)) + if (c < 128 && isOpeningCharacter[c]) { return i; } @@ -267,35 +229,70 @@ private int IndexOfOpeningCharacterNonAscii(string text, int start, int end) #endif return -1; } - } - - internal unsafe struct BoolVector128 - { - private fixed bool values[128]; - - public void Set(char c) + else { - Debug.Assert(c < 128); - values[c] = true; + return IndexOfOpeningCharacterNonAscii(text, start, end); } + } - public readonly bool this[uint c] + private int IndexOfOpeningCharacterNonAscii(string text, int start, int end) + { +#if NETCOREAPP3_1_OR_GREATER + ref char textRef = ref Unsafe.AsRef(in text.GetPinnableReference()); + for (int i = start; i <= end; i++) { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get + char c = Unsafe.Add(ref textRef, i); + if (c < 128 ? isOpeningCharacter[c] : nonAsciiMap!.ContainsKey(c)) { - Debug.Assert(c < 128); - return values[c]; + return i; } } - public readonly bool this[ulong c] +#else + unsafe { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get + fixed (char* pText = text) { - Debug.Assert(c < 128 && IntPtr.Size == 8); - return values[c]; + for (int i = start; i <= end; i++) + { + char c = pText[i]; + if (c < 128 ? isOpeningCharacter[c] : nonAsciiMap!.ContainsKey(c)) + { + return i; + } + } } } +#endif + return -1; + } +} + +internal unsafe struct BoolVector128 +{ + private fixed bool values[128]; + + public void Set(char c) + { + Debug.Assert(c < 128); + values[c] = true; + } + + public readonly bool this[uint c] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + Debug.Assert(c < 128); + return values[c]; + } + } + public readonly bool this[ulong c] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + Debug.Assert(c < 128 && IntPtr.Size == 8); + return values[c]; + } } } \ No newline at end of file diff --git a/src/Markdig/Helpers/CompactPrefixTree.cs b/src/Markdig/Helpers/CompactPrefixTree.cs index 7a239e121..1d7c45462 100644 --- a/src/Markdig/Helpers/CompactPrefixTree.cs +++ b/src/Markdig/Helpers/CompactPrefixTree.cs @@ -3,9 +3,7 @@ #nullable disable -using System; using System.Collections; -using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -20,981 +18,980 @@ */ //namespace SharpCollections.Generic -namespace Markdig.Helpers +namespace Markdig.Helpers; + +/// +/// A compact insert-only key/value collection for fast prefix lookups +/// Something between a Trie and a full Radix tree, but stored linearly in memory +/// +/// The value associated with the key +[ExcludeFromCodeCoverage] +internal sealed class CompactPrefixTree : IReadOnlyDictionary, IReadOnlyList> { /// - /// A compact insert-only key/value collection for fast prefix lookups - /// Something between a Trie and a full Radix tree, but stored linearly in memory + /// Used internally to control behavior of insertion + /// Copied from internals /// - /// The value associated with the key - [ExcludeFromCodeCoverage] - internal sealed class CompactPrefixTree : IReadOnlyDictionary, IReadOnlyList> + internal enum InsertionBehavior : byte { /// - /// Used internally to control behavior of insertion - /// Copied from internals + /// The default insertion behavior. Does not overwrite or throw. /// - internal enum InsertionBehavior : byte - { - /// - /// The default insertion behavior. Does not overwrite or throw. - /// - None = 0, - - /// - /// Specifies that an existing entry with the same key should be overwritten if encountered. - /// - OverwriteExisting = 1, - - /// - /// Specifies that if an existing entry with the same key is encountered, an exception should be thrown. - /// - ThrowOnExisting = 2 - } - - [DebuggerDisplay("{Char}, Child: {ChildChar} at {ChildIndex}, Match: {MatchIndex}, Children: {Children?.Count ?? 0}")] - private struct Node - { - /// - /// The character this node represents, should never be 0 - /// - public char Char; - /// - /// Will be 0 if this is a leaf node - /// - public char ChildChar; - public int ChildIndex; - /// - /// Set to -1 if it does not point to a match - /// - public int MatchIndex; - /// - /// -1 if not present - /// - public int Children; - } - - private Node[] _tree; - private static readonly Node[] _emptyTree = new Node[0]; + None = 0, - private KeyValuePair[] _matches; - private static readonly KeyValuePair[] _emptyMatches = new KeyValuePair[0]; - - private int _childrenIndex = 0; - private int[] _children = _emptyChildren; - private static readonly int[] _emptyChildren = new int[0]; + /// + /// Specifies that an existing entry with the same key should be overwritten if encountered. + /// + OverwriteExisting = 1, - #region Size and Capacity + /// + /// Specifies that if an existing entry with the same key is encountered, an exception should be thrown. + /// + ThrowOnExisting = 2 + } + [DebuggerDisplay("{Char}, Child: {ChildChar} at {ChildIndex}, Match: {MatchIndex}, Children: {Children?.Count ?? 0}")] + private struct Node + { + /// + /// The character this node represents, should never be 0 + /// + public char Char; + /// + /// Will be 0 if this is a leaf node + /// + public char ChildChar; + public int ChildIndex; /// - /// Gets the number of nodes in the internal tree structure - /// You might be looking for - /// Exposing this might help in deducing more efficient initial parameters + /// Set to -1 if it does not point to a match /// - public int TreeSize { get; private set; } + public int MatchIndex; /// - /// Gets or sets the capacity of the internal tree structure buffer - /// You might be looking for + /// -1 if not present /// - public int TreeCapacity + public int Children; + } + + private Node[] _tree; + private static readonly Node[] _emptyTree = new Node[0]; + + private KeyValuePair[] _matches; + private static readonly KeyValuePair[] _emptyMatches = new KeyValuePair[0]; + + private int _childrenIndex = 0; + private int[] _children = _emptyChildren; + private static readonly int[] _emptyChildren = new int[0]; + + #region Size and Capacity + + /// + /// Gets the number of nodes in the internal tree structure + /// You might be looking for + /// Exposing this might help in deducing more efficient initial parameters + /// + public int TreeSize { get; private set; } + /// + /// Gets or sets the capacity of the internal tree structure buffer + /// You might be looking for + /// + public int TreeCapacity + { + get { - get - { - return _tree.Length; - } - set - { - if (value < TreeSize) - ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionReason.SmallCapacity); + return _tree.Length; + } + set + { + if (value < TreeSize) + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionReason.SmallCapacity); - if (value != TreeSize) + if (value != TreeSize) + { + Node[] newTree = new Node[value]; + if (TreeSize > 0) { - Node[] newTree = new Node[value]; - if (TreeSize > 0) - { - Array.Copy(_tree, 0, newTree, 0, TreeSize); - } - _tree = newTree; + Array.Copy(_tree, 0, newTree, 0, TreeSize); } + _tree = newTree; } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void EnsureTreeCapacity(int min) + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureTreeCapacity(int min) + { + if (_tree.Length < min) { - if (_tree.Length < min) - { - EnsureTreeCapacityRare(min); - } - Debug.Assert(_tree.Length >= min); - } - [MethodImpl(MethodImplOptions.NoInlining)] - private void EnsureTreeCapacityRare(int min) - { - // Expansion logic as in System.Collections.Generic.List - Debug.Assert(min > _tree.Length); - int newCapacity = _tree.Length * 2; - if ((uint)min > int.MaxValue) newCapacity = int.MaxValue; - if (newCapacity < min) newCapacity = min; - TreeCapacity = newCapacity; + EnsureTreeCapacityRare(min); } + Debug.Assert(_tree.Length >= min); + } + [MethodImpl(MethodImplOptions.NoInlining)] + private void EnsureTreeCapacityRare(int min) + { + // Expansion logic as in System.Collections.Generic.List + Debug.Assert(min > _tree.Length); + int newCapacity = _tree.Length * 2; + if ((uint)min > int.MaxValue) newCapacity = int.MaxValue; + if (newCapacity < min) newCapacity = min; + TreeCapacity = newCapacity; + } - /// - /// Gets the number of key/value pairs contained in the - /// - public int Count { get; private set; } - /// - /// Gets or sets the capacity of the internal key/value pair buffer - /// - public int Capacity + /// + /// Gets the number of key/value pairs contained in the + /// + public int Count { get; private set; } + /// + /// Gets or sets the capacity of the internal key/value pair buffer + /// + public int Capacity + { + get { - get - { - return _matches.Length; - } - set - { - if (value < Count) - ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionReason.SmallCapacity); + return _matches.Length; + } + set + { + if (value < Count) + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionReason.SmallCapacity); - if (value != Count) + if (value != Count) + { + KeyValuePair[] newMatches = new KeyValuePair[value]; + if (Count > 0) { - KeyValuePair[] newMatches = new KeyValuePair[value]; - if (Count > 0) - { - Array.Copy(_matches, 0, newMatches, 0, Count); - } - _matches = newMatches; + Array.Copy(_matches, 0, newMatches, 0, Count); } + _matches = newMatches; } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void EnsureCapacity(int min) + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureCapacity(int min) + { + // Expansion logic as in System.Collections.Generic.List + if (_matches.Length < min) { - // Expansion logic as in System.Collections.Generic.List - if (_matches.Length < min) - { - EnsureCapacityRare(min); - } - Debug.Assert(_matches.Length >= min); - } - [MethodImpl(MethodImplOptions.NoInlining)] - private void EnsureCapacityRare(int min) - { - // Expansion logic as in System.Collections.Generic.List - Debug.Assert(min > _matches.Length); - int newCapacity = _matches.Length * 2; - if ((uint)min > int.MaxValue) newCapacity = int.MaxValue; - if (newCapacity < min) newCapacity = min; - Capacity = newCapacity; + EnsureCapacityRare(min); } + Debug.Assert(_matches.Length >= min); + } + [MethodImpl(MethodImplOptions.NoInlining)] + private void EnsureCapacityRare(int min) + { + // Expansion logic as in System.Collections.Generic.List + Debug.Assert(min > _matches.Length); + int newCapacity = _matches.Length * 2; + if ((uint)min > int.MaxValue) newCapacity = int.MaxValue; + if (newCapacity < min) newCapacity = min; + Capacity = newCapacity; + } - /// - /// Gets the size of the children buffer in the internal tree structure - /// You might be looking for - /// Exposing this might help in deducing more efficient initial parameters - /// - public int ChildrenCount => _childrenIndex; - /// - /// Gets or sets the capacity of the internal children buffer - /// You might be looking for - /// - public int ChildrenCapacity + /// + /// Gets the size of the children buffer in the internal tree structure + /// You might be looking for + /// Exposing this might help in deducing more efficient initial parameters + /// + public int ChildrenCount => _childrenIndex; + /// + /// Gets or sets the capacity of the internal children buffer + /// You might be looking for + /// + public int ChildrenCapacity + { + get { - get - { - return _children.Length; - } - set - { - if (value < _childrenIndex) - ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionReason.SmallCapacity); + return _children.Length; + } + set + { + if (value < _childrenIndex) + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionReason.SmallCapacity); - if (value != _childrenIndex) + if (value != _childrenIndex) + { + int[] newChildren = new int[value]; + if (_childrenIndex > 0) { - int[] newChildren = new int[value]; - if (_childrenIndex > 0) - { - Array.Copy(_children, 0, newChildren, 0, _childrenIndex); - } + Array.Copy(_children, 0, newChildren, 0, _childrenIndex); + } - // Set new odd indexes to -1 - for (int i = _childrenIndex + 1; i < newChildren.Length; i += 2) - newChildren[i] = -1; + // Set new odd indexes to -1 + for (int i = _childrenIndex + 1; i < newChildren.Length; i += 2) + newChildren[i] = -1; - _children = newChildren; - } + _children = newChildren; } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void EnsureChildrenCapacity(int min) + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureChildrenCapacity(int min) + { + if (_children.Length < min) { - if (_children.Length < min) - { - EnsureChildrenCapacityRare(min); - } - Debug.Assert(_children.Length >= min); - } - [MethodImpl(MethodImplOptions.NoInlining)] - private void EnsureChildrenCapacityRare(int min) - { - // Expansion logic as in System.Collections.Generic.List - Debug.Assert(min > _children.Length); - Debug.Assert(_childrenIndex % 2 == 0); - int newCapacity = _children.Length * 2; - if ((uint)min > int.MaxValue) newCapacity = int.MaxValue; - if (newCapacity < min) newCapacity = min; - ChildrenCapacity = newCapacity; + EnsureChildrenCapacityRare(min); } + Debug.Assert(_children.Length >= min); + } + [MethodImpl(MethodImplOptions.NoInlining)] + private void EnsureChildrenCapacityRare(int min) + { + // Expansion logic as in System.Collections.Generic.List + Debug.Assert(min > _children.Length); + Debug.Assert(_childrenIndex % 2 == 0); + int newCapacity = _children.Length * 2; + if ((uint)min > int.MaxValue) newCapacity = int.MaxValue; + if (newCapacity < min) newCapacity = min; + ChildrenCapacity = newCapacity; + } - #endregion Size and Capacity + #endregion Size and Capacity - #region RootChar + #region RootChar - // Inspired by Markdig's CharacterMap + // Inspired by Markdig's CharacterMap - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool TryGetRoot(char rootChar, out int rootNodeIndex) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool TryGetRoot(char rootChar, out int rootNodeIndex) + { + if (rootChar < 128) { - if (rootChar < 128) - { - rootNodeIndex = _asciiRootMap[rootChar]; - return rootNodeIndex != -1; - } + rootNodeIndex = _asciiRootMap[rootChar]; + return rootNodeIndex != -1; + } - if (_unicodeRootMap != null) - { - return _unicodeRootMap.TryGetValue(rootChar, out rootNodeIndex); - } + if (_unicodeRootMap != null) + { + return _unicodeRootMap.TryGetValue(rootChar, out rootNodeIndex); + } - rootNodeIndex = -1; - return false; + rootNodeIndex = -1; + return false; + } + private void SetRootChar(char rootChar) + { + if (rootChar < 128) + { + Debug.Assert(_asciiRootMap[rootChar] == -1); + _asciiRootMap[rootChar] = TreeSize; } - private void SetRootChar(char rootChar) + else { - if (rootChar < 128) + if (_unicodeRootMap is null) { - Debug.Assert(_asciiRootMap[rootChar] == -1); - _asciiRootMap[rootChar] = TreeSize; - } - else - { - if (_unicodeRootMap is null) - { - _unicodeRootMap = new Dictionary(); - } - _unicodeRootMap.Add(rootChar, TreeSize); + _unicodeRootMap = new Dictionary(); } + _unicodeRootMap.Add(rootChar, TreeSize); } - private readonly int[] _asciiRootMap = new int[128]; - private Dictionary _unicodeRootMap; + } + private readonly int[] _asciiRootMap = new int[128]; + private Dictionary _unicodeRootMap; - #endregion RootChar + #endregion RootChar - private void Init(int matchCapacity, int treeCapacity, int childrenCapacity) - { - for (int i = 0; i < _asciiRootMap.Length; i++) - _asciiRootMap[i] = -1; + private void Init(int matchCapacity, int treeCapacity, int childrenCapacity) + { + for (int i = 0; i < _asciiRootMap.Length; i++) + _asciiRootMap[i] = -1; - _matches = matchCapacity == 0 ? _emptyMatches : new KeyValuePair[matchCapacity]; - _tree = treeCapacity == 0 ? _emptyTree : new Node[treeCapacity]; - EnsureChildrenCapacity(childrenCapacity); - } + _matches = matchCapacity == 0 ? _emptyMatches : new KeyValuePair[matchCapacity]; + _tree = treeCapacity == 0 ? _emptyTree : new Node[treeCapacity]; + EnsureChildrenCapacity(childrenCapacity); + } - /// - /// Constructs a new with no initial prefixes - /// - public CompactPrefixTree(int matchCapacity = 0, int treeCapacity = 0, int childrenCapacity = 0) - { - Init(matchCapacity, treeCapacity, childrenCapacity); - } - /// - /// Constructs a new with the supplied matches - /// - /// Matches to initialize the with. For best lookup performance, this collection should be sorted. - public CompactPrefixTree(ICollection> input) - { - if (input is null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.input); + /// + /// Constructs a new with no initial prefixes + /// + public CompactPrefixTree(int matchCapacity = 0, int treeCapacity = 0, int childrenCapacity = 0) + { + Init(matchCapacity, treeCapacity, childrenCapacity); + } + /// + /// Constructs a new with the supplied matches + /// + /// Matches to initialize the with. For best lookup performance, this collection should be sorted. + public CompactPrefixTree(ICollection> input) + { + if (input is null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.input); - Init(input.Count, input.Count * 2, input.Count * 2); + Init(input.Count, input.Count * 2, input.Count * 2); - using (var e = input.GetEnumerator()) + using (var e = input.GetEnumerator()) + { + for (int i = 0; i < input.Count; i++) { - for (int i = 0; i < input.Count; i++) - { - e.MoveNext(); - TryInsert(e.Current, InsertionBehavior.ThrowOnExisting); - } + e.MoveNext(); + TryInsert(e.Current, InsertionBehavior.ThrowOnExisting); } } + } - #region this[] accessors + #region this[] accessors - /// - /// Retrieves the key/value pair at the specified index (must be lower than ) - /// - /// Index of pair to get, must be lower than (the order is the same as the order in which the elements were added) - /// The key/value pair of the element at the specified index - public KeyValuePair this[int index] + /// + /// Retrieves the key/value pair at the specified index (must be lower than ) + /// + /// Index of pair to get, must be lower than (the order is the same as the order in which the elements were added) + /// The key/value pair of the element at the specified index + public KeyValuePair this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - if ((uint)index >= (uint)Count) ThrowHelper.ThrowIndexOutOfRangeException(); - return _matches[index]; - } + if ((uint)index >= (uint)Count) ThrowHelper.ThrowIndexOutOfRangeException(); + return _matches[index]; } + } - /// - /// Gets or sets the value associated with the specified key - /// - /// The key of the value to get or set - /// The value of the element with the specified key - public TValue this[string key] - { - get - { - if (TryMatchExact(key.AsSpan(), out KeyValuePair match)) - return match.Value; - throw new KeyNotFoundException(key); - } - set - { - var pair = new KeyValuePair(key, value); - bool modified = TryInsert(in pair, InsertionBehavior.OverwriteExisting); - Debug.Assert(modified); - } - } // Get, Set - - /// - /// Gets the value associated with the specified key - /// - /// The key of the value to get - /// The key/value pair of the element with the specified key - public KeyValuePair this[ReadOnlySpan key] + /// + /// Gets or sets the value associated with the specified key + /// + /// The key of the value to get or set + /// The value of the element with the specified key + public TValue this[string key] + { + get { - get - { - if (TryMatchExact(key, out KeyValuePair match)) - return match; - throw new KeyNotFoundException(key.ToString()); - } - } // Get only - - #endregion this[] accessors - - #region Add, TryAdd - - /// - /// Adds the specified key/value pair to the - /// - /// The key of the element to add - /// The value of the element to add - public void Add(string key, TValue value) + if (TryMatchExact(key.AsSpan(), out KeyValuePair match)) + return match.Value; + throw new KeyNotFoundException(key); + } + set { var pair = new KeyValuePair(key, value); - TryInsert(in pair, InsertionBehavior.ThrowOnExisting); + bool modified = TryInsert(in pair, InsertionBehavior.OverwriteExisting); + Debug.Assert(modified); } - /// - /// Adds the specified key/value pair to the - /// - /// The key/value pair to add - public void Add(KeyValuePair pair) - => TryInsert(in pair, InsertionBehavior.ThrowOnExisting); + } // Get, Set - /// - /// Tries to add the key/value pair to the if the key is not yet present - /// - /// The key of the element to add - /// The value of the element to add - /// True if the element was added, false otherwise - public bool TryAdd(string key, TValue value) + /// + /// Gets the value associated with the specified key + /// + /// The key of the value to get + /// The key/value pair of the element with the specified key + public KeyValuePair this[ReadOnlySpan key] + { + get { - var pair = new KeyValuePair(key, value); - return TryInsert(in pair, InsertionBehavior.None); + if (TryMatchExact(key, out KeyValuePair match)) + return match; + throw new KeyNotFoundException(key.ToString()); } - /// - /// Tries to add the key/value pair to the if the key is not yet present - /// - /// The pair to add - /// True if the element was added, false otherwise - public bool TryAdd(KeyValuePair pair) - => TryInsert(in pair, InsertionBehavior.None); + } // Get only - #endregion Add, TryAdd + #endregion this[] accessors - #region Insert internal + #region Add, TryAdd - private bool TryInsert(in KeyValuePair pair, InsertionBehavior behavior) - { - string key = pair.Key; - if (key is null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); - if (key.Length == 0) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.key, ExceptionReason.String_Empty); - Debug.Assert(!string.IsNullOrEmpty(key)); + /// + /// Adds the specified key/value pair to the + /// + /// The key of the element to add + /// The value of the element to add + public void Add(string key, TValue value) + { + var pair = new KeyValuePair(key, value); + TryInsert(in pair, InsertionBehavior.ThrowOnExisting); + } + /// + /// Adds the specified key/value pair to the + /// + /// The key/value pair to add + public void Add(KeyValuePair pair) + => TryInsert(in pair, InsertionBehavior.ThrowOnExisting); + + /// + /// Tries to add the key/value pair to the if the key is not yet present + /// + /// The key of the element to add + /// The value of the element to add + /// True if the element was added, false otherwise + public bool TryAdd(string key, TValue value) + { + var pair = new KeyValuePair(key, value); + return TryInsert(in pair, InsertionBehavior.None); + } + /// + /// Tries to add the key/value pair to the if the key is not yet present + /// + /// The pair to add + /// True if the element was added, false otherwise + public bool TryAdd(KeyValuePair pair) + => TryInsert(in pair, InsertionBehavior.None); - char rootChar = key[0]; - if (TryGetRoot(rootChar, out int rootNodeIndex)) + #endregion Add, TryAdd + + #region Insert internal + + private bool TryInsert(in KeyValuePair pair, InsertionBehavior behavior) + { + string key = pair.Key; + if (key is null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); + if (key.Length == 0) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.key, ExceptionReason.String_Empty); + Debug.Assert(!string.IsNullOrEmpty(key)); + + char rootChar = key[0]; + if (TryGetRoot(rootChar, out int rootNodeIndex)) + { + var tree = _tree; + ref Node node = ref tree[rootNodeIndex]; + for (int i = 1; i < key.Length; i++) { - var tree = _tree; - ref Node node = ref tree[rootNodeIndex]; - for (int i = 1; i < key.Length; i++) + char c = key[i]; + + if (node.ChildChar == c) { - char c = key[i]; + node = ref tree[node.ChildIndex]; + continue; + } - if (node.ChildChar == c) + if (node.Children == -1) + { + // This could be a leaf node + if (node.ChildChar == 0) { - node = ref tree[node.ChildIndex]; - continue; - } + // This is a leaf node + int previousMatchIndex = node.MatchIndex; + ref KeyValuePair previousMatch = ref _matches[previousMatchIndex]; + string previousKey = previousMatch.Key; - if (node.Children == -1) - { - // This could be a leaf node - if (node.ChildChar == 0) + // Find out for how long the two keys continue to share a prefix + int previousIndex = i; + int minLength = Math.Min(key.Length, previousKey.Length); + for (; i < minLength; i++) { - // This is a leaf node - int previousMatchIndex = node.MatchIndex; - ref KeyValuePair previousMatch = ref _matches[previousMatchIndex]; - string previousKey = previousMatch.Key; - - // Find out for how long the two keys continue to share a prefix - int previousIndex = i; - int minLength = Math.Min(key.Length, previousKey.Length); - for (; i < minLength; i++) - { - // We haven't checked the i-th character of key so far - if (key[i] != previousKey[i]) break; - } + // We haven't checked the i-th character of key so far + if (key[i] != previousKey[i]) break; + } - if (i == minLength && key.Length == previousKey.Length) - { - // The two keys are of the same length and i has reached the length of the key => duplicate - Debug.Assert(i == key.Length && i == previousKey.Length); - Debug.Assert(key == previousKey); - goto HandleDuplicateKey; - } + if (i == minLength && key.Length == previousKey.Length) + { + // The two keys are of the same length and i has reached the length of the key => duplicate + Debug.Assert(i == key.Length && i == previousKey.Length); + Debug.Assert(key == previousKey); + goto HandleDuplicateKey; + } - // Clear the match index on the current node as it's no longer a leaf node - node.MatchIndex = -1; + // Clear the match index on the current node as it's no longer a leaf node + node.MatchIndex = -1; - // insert (i - previousIndex) intermediary nodes (could be 0) - int intermediaryNodesToInsert = i - previousIndex; - if (intermediaryNodesToInsert > 0) + // insert (i - previousIndex) intermediary nodes (could be 0) + int intermediaryNodesToInsert = i - previousIndex; + if (intermediaryNodesToInsert > 0) + { + node.ChildIndex = TreeSize; + node.ChildChar = key[previousIndex]; + EnsureTreeCapacity(TreeSize + intermediaryNodesToInsert); + tree = _tree; + for (int j = 0; j < intermediaryNodesToInsert - 1; j++) { - node.ChildIndex = TreeSize; - node.ChildChar = key[previousIndex]; - EnsureTreeCapacity(TreeSize + intermediaryNodesToInsert); - tree = _tree; - for (int j = 0; j < intermediaryNodesToInsert - 1; j++) + tree[TreeSize + j] = new Node() { - tree[TreeSize + j] = new Node() - { - Char = previousKey[previousIndex + j], - ChildChar = previousKey[previousIndex + j + 1], - ChildIndex = TreeSize + j + 1, - MatchIndex = -1, - Children = -1 - }; - } - TreeSize += intermediaryNodesToInsert; - tree[TreeSize - 1] = new Node() - { - Char = previousKey[previousIndex + intermediaryNodesToInsert - 1], + Char = previousKey[previousIndex + j], + ChildChar = previousKey[previousIndex + j + 1], + ChildIndex = TreeSize + j + 1, MatchIndex = -1, Children = -1 }; - node = ref tree[TreeSize - 1]; } + TreeSize += intermediaryNodesToInsert; + tree[TreeSize - 1] = new Node() + { + Char = previousKey[previousIndex + intermediaryNodesToInsert - 1], + MatchIndex = -1, + Children = -1 + }; + node = ref tree[TreeSize - 1]; + } - node.ChildIndex = TreeSize; + node.ChildIndex = TreeSize; - // Insert the pair - EnsureCapacity(Count + 1); - _matches[Count] = pair; + // Insert the pair + EnsureCapacity(Count + 1); + _matches[Count] = pair; - // One of the strings could be a prefix of the other, in that case we're only inserting one leaf node - if (i == minLength) + // One of the strings could be a prefix of the other, in that case we're only inserting one leaf node + if (i == minLength) + { + Debug.Assert(key.Length != previousKey.Length); + if (previousKey.Length < key.Length) // If the input was sorted, this should be hit { - Debug.Assert(key.Length != previousKey.Length); - if (previousKey.Length < key.Length) // If the input was sorted, this should be hit + Debug.Assert(key.StartsWith(previousKey, StringComparison.Ordinal)); + node.ChildChar = key[i]; + node.MatchIndex = previousMatchIndex; + EnsureTreeCapacity(TreeSize + 1); + _tree[TreeSize] = new Node() { - Debug.Assert(key.StartsWith(previousKey, StringComparison.Ordinal)); - node.ChildChar = key[i]; - node.MatchIndex = previousMatchIndex; - EnsureTreeCapacity(TreeSize + 1); - _tree[TreeSize] = new Node() - { - Char = key[i], - MatchIndex = Count, - Children = -1 - }; - } - else // if key.Length < previousKey.Length + Char = key[i], + MatchIndex = Count, + Children = -1 + }; + } + else // if key.Length < previousKey.Length + { + Debug.Assert(key.Length < previousKey.Length); + Debug.Assert(previousKey.StartsWith(key, StringComparison.Ordinal)); + node.ChildChar = previousKey[i]; + node.MatchIndex = Count; + EnsureTreeCapacity(TreeSize + 1); + _tree[TreeSize] = new Node() { - Debug.Assert(key.Length < previousKey.Length); - Debug.Assert(previousKey.StartsWith(key, StringComparison.Ordinal)); - node.ChildChar = previousKey[i]; - node.MatchIndex = Count; - EnsureTreeCapacity(TreeSize + 1); - _tree[TreeSize] = new Node() - { - Char = previousKey[i], - MatchIndex = previousMatchIndex, - Children = -1 - }; - } - Count++; - TreeSize++; - return true; + Char = previousKey[i], + MatchIndex = previousMatchIndex, + Children = -1 + }; } + Count++; + TreeSize++; + return true; + } - // Insert two leaf nodes - Debug.Assert(node.Char != 0 && node.Char == previousKey[i - 1]); - Debug.Assert(node.MatchIndex == -1); - Debug.Assert(node.Children == -1); - - node.ChildChar = previousKey[i]; - node.Children = _childrenIndex; + // Insert two leaf nodes + Debug.Assert(node.Char != 0 && node.Char == previousKey[i - 1]); + Debug.Assert(node.MatchIndex == -1); + Debug.Assert(node.Children == -1); - EnsureChildrenCapacity(_childrenIndex + 2); - _children[_childrenIndex] = TreeSize + 1; - _childrenIndex += 2; + node.ChildChar = previousKey[i]; + node.Children = _childrenIndex; - // Insert the two leaf nodes - EnsureTreeCapacity(TreeSize + 2); - _tree[TreeSize] = new Node() - { - Char = previousKey[i], - MatchIndex = previousMatchIndex, - Children = -1 - }; - _tree[TreeSize + 1] = new Node() - { - Char = key[i], - MatchIndex = Count, - Children = -1 - }; + EnsureChildrenCapacity(_childrenIndex + 2); + _children[_childrenIndex] = TreeSize + 1; + _childrenIndex += 2; - Count++; - TreeSize += 2; - return true; - } - else + // Insert the two leaf nodes + EnsureTreeCapacity(TreeSize + 2); + _tree[TreeSize] = new Node() { - // This node has a child char, therefore we either don't have a match attached or that match is simply a prefix of the current key - Debug.Assert(node.MatchIndex == -1 || key.StartsWith(_matches[node.MatchIndex].Key, StringComparison.Ordinal)); - - // Set this pair as the current node's first element in the Children list - node.Children = _childrenIndex; - EnsureChildrenCapacity(_childrenIndex + 2); - _children[_childrenIndex] = TreeSize; - _childrenIndex += 2; + Char = previousKey[i], + MatchIndex = previousMatchIndex, + Children = -1 + }; + _tree[TreeSize + 1] = new Node() + { + Char = key[i], + MatchIndex = Count, + Children = -1 + }; - InsertLeafNode(in pair, c); - return true; - } + Count++; + TreeSize += 2; + return true; } else { - // Look for a child node with a matching Char in all of children - var children = _children; - int childrenIndex = node.Children; - int lastChildrenIndex = childrenIndex; - do - { - if ((uint)childrenIndex >= (uint)children.Length) - break; - node = ref _tree[children[childrenIndex]]; - if (node.Char == c) goto NextChar; - lastChildrenIndex = childrenIndex; - childrenIndex = children[childrenIndex + 1]; - } - while (true); + // This node has a child char, therefore we either don't have a match attached or that match is simply a prefix of the current key + Debug.Assert(node.MatchIndex == -1 || key.StartsWith(_matches[node.MatchIndex].Key, StringComparison.Ordinal)); - // A child node was not found, add a new one to children + // Set this pair as the current node's first element in the Children list + node.Children = _childrenIndex; EnsureChildrenCapacity(_childrenIndex + 2); - _children[lastChildrenIndex + 1] = _childrenIndex; _children[_childrenIndex] = TreeSize; _childrenIndex += 2; InsertLeafNode(in pair, c); return true; } - - NextChar:; } - - // We have found our final node, check if a match already claimed this node - if (node.MatchIndex != -1) + else { - ref KeyValuePair previousMatch = ref _matches[node.MatchIndex]; - - // Either some other key is the leaf here, or the key is duplicated - if (previousMatch.Key.Length == key.Length) + // Look for a child node with a matching Char in all of children + var children = _children; + int childrenIndex = node.Children; + int lastChildrenIndex = childrenIndex; + do { - Debug.Assert(previousMatch.Key == key); - goto HandleDuplicateKey; + if ((uint)childrenIndex >= (uint)children.Length) + break; + node = ref _tree[children[childrenIndex]]; + if (node.Char == c) goto NextChar; + lastChildrenIndex = childrenIndex; + childrenIndex = children[childrenIndex + 1]; } - else - { - // It's not a duplicate but shares key.Length characters, therefore it's longer - // This will never occur if the input was sorted - Debug.Assert(previousMatch.Key.Length > key.Length); - Debug.Assert(previousMatch.Key.StartsWith(key, StringComparison.Ordinal)); - Debug.Assert(node.ChildChar == 0 && node.Children == -1); - - // It is a leaf node - // Move the prevMatch one node inward - int previousMatchIndex = node.MatchIndex; - node.MatchIndex = Count; - node.ChildChar = previousMatch.Key[key.Length]; - node.ChildIndex = TreeSize; - EnsureTreeCapacity(TreeSize + 1); - _tree[TreeSize] = new Node() - { - Char = previousMatch.Key[key.Length], - MatchIndex = previousMatchIndex, - Children = -1 - }; - TreeSize++; + while (true); - // Set the pair as a match on this node - } + // A child node was not found, add a new one to children + EnsureChildrenCapacity(_childrenIndex + 2); + _children[lastChildrenIndex + 1] = _childrenIndex; + _children[_childrenIndex] = TreeSize; + _childrenIndex += 2; + + InsertLeafNode(in pair, c); + return true; } - // Set the pair as a match on this node - node.MatchIndex = Count; // This might be modifying a forgotten node reference, but in that case it was already set - EnsureCapacity(Count + 1); - _matches[Count] = pair; - Count++; - return true; + NextChar:; + } - HandleDuplicateKey:; - Debug.Assert(key == _matches[node.MatchIndex].Key); - if (behavior == InsertionBehavior.None) return false; - if (behavior == InsertionBehavior.OverwriteExisting) + // We have found our final node, check if a match already claimed this node + if (node.MatchIndex != -1) + { + ref KeyValuePair previousMatch = ref _matches[node.MatchIndex]; + + // Either some other key is the leaf here, or the key is duplicated + if (previousMatch.Key.Length == key.Length) { - _matches[node.MatchIndex] = pair; - return true; + Debug.Assert(previousMatch.Key == key); + goto HandleDuplicateKey; + } + else + { + // It's not a duplicate but shares key.Length characters, therefore it's longer + // This will never occur if the input was sorted + Debug.Assert(previousMatch.Key.Length > key.Length); + Debug.Assert(previousMatch.Key.StartsWith(key, StringComparison.Ordinal)); + Debug.Assert(node.ChildChar == 0 && node.Children == -1); + + // It is a leaf node + // Move the prevMatch one node inward + int previousMatchIndex = node.MatchIndex; + node.MatchIndex = Count; + node.ChildChar = previousMatch.Key[key.Length]; + node.ChildIndex = TreeSize; + EnsureTreeCapacity(TreeSize + 1); + _tree[TreeSize] = new Node() + { + Char = previousMatch.Key[key.Length], + MatchIndex = previousMatchIndex, + Children = -1 + }; + TreeSize++; + + // Set the pair as a match on this node } - Debug.Assert(behavior == InsertionBehavior.ThrowOnExisting); - ThrowHelper.ThrowArgumentException(ExceptionArgument.key, ExceptionReason.DuplicateKey); - Debug.Assert(false, "Should throw by now"); - return false; } - else // if the root character is not yet in the collection + + // Set the pair as a match on this node + node.MatchIndex = Count; // This might be modifying a forgotten node reference, but in that case it was already set + EnsureCapacity(Count + 1); + _matches[Count] = pair; + Count++; + return true; + + HandleDuplicateKey:; + Debug.Assert(key == _matches[node.MatchIndex].Key); + if (behavior == InsertionBehavior.None) return false; + if (behavior == InsertionBehavior.OverwriteExisting) { - SetRootChar(rootChar); - InsertLeafNode(in pair, rootChar); + _matches[node.MatchIndex] = pair; return true; } + Debug.Assert(behavior == InsertionBehavior.ThrowOnExisting); + ThrowHelper.ThrowArgumentException(ExceptionArgument.key, ExceptionReason.DuplicateKey); + Debug.Assert(false, "Should throw by now"); + return false; } - private void InsertLeafNode(in KeyValuePair pair, char nodeChar) + else // if the root character is not yet in the collection { - EnsureCapacity(Count + 1); - _matches[Count] = pair; - - EnsureTreeCapacity(TreeSize + 1); - _tree[TreeSize] = new Node() - { - Char = nodeChar, - MatchIndex = Count, - Children = -1 - }; - - Count++; - TreeSize++; + SetRootChar(rootChar); + InsertLeafNode(in pair, rootChar); + return true; } + } + private void InsertLeafNode(in KeyValuePair pair, char nodeChar) + { + EnsureCapacity(Count + 1); + _matches[Count] = pair; - #endregion Insert internal + EnsureTreeCapacity(TreeSize + 1); + _tree[TreeSize] = new Node() + { + Char = nodeChar, + MatchIndex = Count, + Children = -1 + }; - #region TryMatch longest + Count++; + TreeSize++; + } - /// - /// Tries to find the longest prefix of text, that is contained in this - /// - /// The text in which to search for the prefix - /// The found prefix and the corresponding value - /// True if a match was found, false otherwise - public bool TryMatchLongest(ReadOnlySpan text, out KeyValuePair match) - { - match = default; - if (text.Length == 0 || !TryGetRoot(text[0], out int nodeIndex)) - return false; + #endregion Insert internal - int matchIndex = -1; - int depth = 1; + #region TryMatch longest - ref Node node = ref _tree[nodeIndex]; - if (node.ChildChar == 0) goto LeafNodeFound; - if (node.MatchIndex != -1) matchIndex = node.MatchIndex; + /// + /// Tries to find the longest prefix of text, that is contained in this + /// + /// The text in which to search for the prefix + /// The found prefix and the corresponding value + /// True if a match was found, false otherwise + public bool TryMatchLongest(ReadOnlySpan text, out KeyValuePair match) + { + match = default; + if (text.Length == 0 || !TryGetRoot(text[0], out int nodeIndex)) + return false; - for (int i = 1; i < text.Length; i++) - { - char c = text[i]; + int matchIndex = -1; + int depth = 1; - if (node.ChildChar == c) - { - node = ref _tree[node.ChildIndex]; - goto NextChar; - } + ref Node node = ref _tree[nodeIndex]; + if (node.ChildChar == 0) goto LeafNodeFound; + if (node.MatchIndex != -1) matchIndex = node.MatchIndex; - var children = _children; - int childrenIndex = node.Children; - do - { - if ((uint)childrenIndex >= (uint)children.Length) - goto Return; - node = ref _tree[children[childrenIndex]]; - if (node.Char == c) goto NextChar; - childrenIndex = children[childrenIndex + 1]; - } - while (true); + for (int i = 1; i < text.Length; i++) + { + char c = text[i]; - NextChar:; - depth++; - if (node.ChildChar == 0) goto LeafNodeFound; - if (node.MatchIndex != -1) matchIndex = node.MatchIndex; + if (node.ChildChar == c) + { + node = ref _tree[node.ChildIndex]; + goto NextChar; } - // We have ran out of our string, return the longest match we've found - goto Return; - LeafNodeFound:; - ref KeyValuePair possibleMatch = ref _matches[node.MatchIndex]; - if (possibleMatch.Key.Length <= text.Length) + var children = _children; + int childrenIndex = node.Children; + do { - // Check that the rest of the strings match - if (text.Slice(depth).StartsWith(possibleMatch.Key.AsSpan(depth), StringComparison.Ordinal)) - { - matchIndex = node.MatchIndex; - } + if ((uint)childrenIndex >= (uint)children.Length) + goto Return; + node = ref _tree[children[childrenIndex]]; + if (node.Char == c) goto NextChar; + childrenIndex = children[childrenIndex + 1]; } + while (true); - Return:; - if (matchIndex != -1) + NextChar:; + depth++; + if (node.ChildChar == 0) goto LeafNodeFound; + if (node.MatchIndex != -1) matchIndex = node.MatchIndex; + } + // We have ran out of our string, return the longest match we've found + goto Return; + + LeafNodeFound:; + ref KeyValuePair possibleMatch = ref _matches[node.MatchIndex]; + if (possibleMatch.Key.Length <= text.Length) + { + // Check that the rest of the strings match + if (text.Slice(depth).StartsWith(possibleMatch.Key.AsSpan(depth), StringComparison.Ordinal)) { - match = _matches[matchIndex]; - return true; + matchIndex = node.MatchIndex; } - return false; } - #endregion TryMatch longest + Return:; + if (matchIndex != -1) + { + match = _matches[matchIndex]; + return true; + } + return false; + } - #region TryMatch exact + #endregion TryMatch longest - /// - /// Tries to find a prefix of text, that is contained in this and is exactly text.Length characters long - /// - /// The text in which to search for the prefix - /// The found prefix and the corresponding value - /// True if a match was found, false otherwise - public bool TryMatchExact(ReadOnlySpan text, out KeyValuePair match) + #region TryMatch exact + + /// + /// Tries to find a prefix of text, that is contained in this and is exactly text.Length characters long + /// + /// The text in which to search for the prefix + /// The found prefix and the corresponding value + /// True if a match was found, false otherwise + public bool TryMatchExact(ReadOnlySpan text, out KeyValuePair match) + { + match = default; + if (text.Length == 0 || !TryGetRoot(text[0], out int nodeIndex)) + return false; + + int depth = 1; + + ref Node node = ref _tree[nodeIndex]; + if (node.ChildChar == 0) goto LeafNodeFound; + if (node.MatchIndex != -1 && text.Length == 1) { - match = default; - if (text.Length == 0 || !TryGetRoot(text[0], out int nodeIndex)) - return false; + match = _matches[node.MatchIndex]; + return true; + } - int depth = 1; + for (int i = 1; i < text.Length; i++) + { + char c = text[i]; - ref Node node = ref _tree[nodeIndex]; - if (node.ChildChar == 0) goto LeafNodeFound; - if (node.MatchIndex != -1 && text.Length == 1) + if (node.ChildChar == c) { - match = _matches[node.MatchIndex]; - return true; + node = ref _tree[node.ChildIndex]; + goto NextChar; } - for (int i = 1; i < text.Length; i++) + var children = _children; + int childrenIndex = node.Children; + do { - char c = text[i]; + if ((uint)childrenIndex >= (uint)children.Length) + return false; + node = ref _tree[children[childrenIndex]]; + if (node.Char == c) goto NextChar; + childrenIndex = children[childrenIndex + 1]; + } + while (true); - if (node.ChildChar == c) - { - node = ref _tree[node.ChildIndex]; - goto NextChar; - } + NextChar:; + depth++; + if (node.ChildChar == 0) goto LeafNodeFound; + } - var children = _children; - int childrenIndex = node.Children; - do - { - if ((uint)childrenIndex >= (uint)children.Length) - return false; - node = ref _tree[children[childrenIndex]]; - if (node.Char == c) goto NextChar; - childrenIndex = children[childrenIndex + 1]; - } - while (true); + if (node.MatchIndex == -1) return false; + match = _matches[node.MatchIndex]; + Debug.Assert(match.Key.Length == text.Length); + return true; - NextChar:; - depth++; - if (node.ChildChar == 0) goto LeafNodeFound; - } + LeafNodeFound:; + match = _matches[node.MatchIndex]; - if (node.MatchIndex == -1) return false; - match = _matches[node.MatchIndex]; - Debug.Assert(match.Key.Length == text.Length); - return true; + return match.Key.Length == text.Length && + text.Slice(depth).Equals(match.Key.AsSpan(depth), StringComparison.Ordinal); + } - LeafNodeFound:; - match = _matches[node.MatchIndex]; + #endregion TryMatch exact - return match.Key.Length == text.Length && - text.Slice(depth).Equals(match.Key.AsSpan(depth), StringComparison.Ordinal); - } + #region TryMatch shortest - #endregion TryMatch exact + /// + /// Tries to find the shortest prefix of text, that is contained in this + /// + /// The text in which to search for the prefix + /// The found prefix and the corresponding value + /// True if a match was found, false otherwise + public bool TryMatchShortest(ReadOnlySpan text, out KeyValuePair match) + { + match = default; + if (text.Length == 0 || !TryGetRoot(text[0], out int nodeIndex)) + return false; - #region TryMatch shortest + ref Node node = ref _tree[nodeIndex]; + if (node.MatchIndex != -1) + { + match = _matches[node.MatchIndex]; + return true; + } - /// - /// Tries to find the shortest prefix of text, that is contained in this - /// - /// The text in which to search for the prefix - /// The found prefix and the corresponding value - /// True if a match was found, false otherwise - public bool TryMatchShortest(ReadOnlySpan text, out KeyValuePair match) + for (int i = 1; i < text.Length; i++) { - match = default; - if (text.Length == 0 || !TryGetRoot(text[0], out int nodeIndex)) - return false; + char c = text[i]; - ref Node node = ref _tree[nodeIndex]; + if (node.ChildChar == c) + { + node = ref _tree[node.ChildIndex]; + goto NextChar; + } + + var children = _children; + int childrenIndex = node.Children; + do + { + if ((uint)childrenIndex >= (uint)children.Length) + return false; + node = ref _tree[children[childrenIndex]]; + if (node.Char == c) goto NextChar; + childrenIndex = children[childrenIndex + 1]; + } + while (true); + + NextChar:; if (node.MatchIndex != -1) { match = _matches[node.MatchIndex]; return true; } + } + Debug.Assert(node.MatchIndex == -1); + return false; + } - for (int i = 1; i < text.Length; i++) - { - char c = text[i]; + #endregion TryMatch shortest - if (node.ChildChar == c) - { - node = ref _tree[node.ChildIndex]; - goto NextChar; - } + #region Interface implementations - var children = _children; - int childrenIndex = node.Children; - do - { - if ((uint)childrenIndex >= (uint)children.Length) - return false; - node = ref _tree[children[childrenIndex]]; - if (node.Char == c) goto NextChar; - childrenIndex = children[childrenIndex + 1]; - } - while (true); + /// + /// Determines whether the contains the specified key + /// + /// The key to locate in this + /// True if the key is contained in this PrefixTree, false otherwise. + public bool ContainsKey(string key) + => TryMatchExact(key.AsSpan(), out _); - NextChar:; - if (node.MatchIndex != -1) - { - match = _matches[node.MatchIndex]; - return true; - } - } - Debug.Assert(node.MatchIndex == -1); - return false; + /// + /// Gets the value associated with the specified key + /// + /// The key of the value to get + /// The value associated with the specified key + /// True if the key is contained in this PrefixTree, false otherwise. + public bool TryGetValue(string key, out TValue value) + { + bool ret = TryMatchExact(key.AsSpan(), out KeyValuePair match); + value = match.Value; + return ret; + } + + /// + /// Gets a collection containing the keys in this + /// + public IEnumerable Keys + { + get + { + for (int i = 0; i < Count; i++) + yield return _matches[i].Key; } + } + /// + /// Gets a collection containing the values in this + /// + public IEnumerable Values + { + get + { + for (int i = 0; i < Count; i++) + yield return _matches[i].Value; + } + } - #endregion TryMatch shortest + /// + /// Returns an Enumerator that iterates through the . + /// Use the index accessor instead () + /// + /// + public IEnumerator> GetEnumerator() => new Enumerator(_matches); - #region Interface implementations + IEnumerator IEnumerable.GetEnumerator() => new Enumerator(_matches); - /// - /// Determines whether the contains the specified key - /// - /// The key to locate in this - /// True if the key is contained in this PrefixTree, false otherwise. - public bool ContainsKey(string key) - => TryMatchExact(key.AsSpan(), out _); + /// + /// Enumerates the elements of a + /// + public struct Enumerator : IEnumerator>, IEnumerator + { + private readonly KeyValuePair[] _matches; + private int _index; - /// - /// Gets the value associated with the specified key - /// - /// The key of the value to get - /// The value associated with the specified key - /// True if the key is contained in this PrefixTree, false otherwise. - public bool TryGetValue(string key, out TValue value) - { - bool ret = TryMatchExact(key.AsSpan(), out KeyValuePair match); - value = match.Value; - return ret; + internal Enumerator(KeyValuePair[] matches) + { + _matches = matches; + _index = -1; } /// - /// Gets a collection containing the keys in this + /// Increments the internal index /// - public IEnumerable Keys - { - get - { - for (int i = 0; i < Count; i++) - yield return _matches[i].Key; - } - } + /// True if the index is less than the length of the internal array + public bool MoveNext() => ++_index < _matches.Length; /// - /// Gets a collection containing the values in this + /// Gets the at the current position /// - public IEnumerable Values - { - get - { - for (int i = 0; i < Count; i++) - yield return _matches[i].Value; - } - } + public KeyValuePair Current => _matches[_index]; + object IEnumerator.Current => _matches[_index]; /// - /// Returns an Enumerator that iterates through the . - /// Use the index accessor instead () + /// Does nothing /// - /// - public IEnumerator> GetEnumerator() => new Enumerator(_matches); - - IEnumerator IEnumerable.GetEnumerator() => new Enumerator(_matches); + public void Dispose() { } /// - /// Enumerates the elements of a + /// Resets the internal index to the beginning of the array /// - public struct Enumerator : IEnumerator>, IEnumerator + public void Reset() { - private readonly KeyValuePair[] _matches; - private int _index; - - internal Enumerator(KeyValuePair[] matches) - { - _matches = matches; - _index = -1; - } - - /// - /// Increments the internal index - /// - /// True if the index is less than the length of the internal array - public bool MoveNext() => ++_index < _matches.Length; - /// - /// Gets the at the current position - /// - public KeyValuePair Current => _matches[_index]; - object IEnumerator.Current => _matches[_index]; - - /// - /// Does nothing - /// - public void Dispose() { } - - /// - /// Resets the internal index to the beginning of the array - /// - public void Reset() - { - _index = -1; - } + _index = -1; } - - #endregion Interface implementations } + + #endregion Interface implementations } diff --git a/src/Markdig/Helpers/CustomArrayPool.cs b/src/Markdig/Helpers/CustomArrayPool.cs index 063738439..29888d92f 100644 --- a/src/Markdig/Helpers/CustomArrayPool.cs +++ b/src/Markdig/Helpers/CustomArrayPool.cs @@ -4,86 +4,85 @@ using System.Threading; -namespace Markdig.Helpers +namespace Markdig.Helpers; + +internal sealed class CustomArrayPool { - internal sealed class CustomArrayPool + private sealed class Bucket { - private sealed class Bucket - { - private readonly T[][] _buffers; + private readonly T[][] _buffers; - private int _index; - private int _lock; + private int _index; + private int _lock; - public Bucket(int numberOfBuffers) - { - _buffers = new T[numberOfBuffers][]; - } + public Bucket(int numberOfBuffers) + { + _buffers = new T[numberOfBuffers][]; + } - public T[] Rent() + public T[] Rent() + { + T[][] buffers = _buffers; + T[] buffer = null!; + if (Interlocked.CompareExchange(ref _lock, 1, 0) == 0) { - T[][] buffers = _buffers; - T[] buffer = null!; - if (Interlocked.CompareExchange(ref _lock, 1, 0) == 0) + int index = _index; + if ((uint)index < (uint)buffers.Length) { - int index = _index; - if ((uint)index < (uint)buffers.Length) - { - buffer = buffers[index]; - buffers[index] = null!; - _index = index + 1; - } - Interlocked.Decrement(ref _lock); + buffer = buffers[index]; + buffers[index] = null!; + _index = index + 1; } - return buffer; + Interlocked.Decrement(ref _lock); } + return buffer; + } - public void Return(T[] array) + public void Return(T[] array) + { + var buffers = _buffers; + if (Interlocked.CompareExchange(ref _lock, 1, 0) == 0) { - var buffers = _buffers; - if (Interlocked.CompareExchange(ref _lock, 1, 0) == 0) + int index = _index - 1; + if ((uint)index < (uint)buffers.Length) { - int index = _index - 1; - if ((uint)index < (uint)buffers.Length) - { - buffers[index] = array; - _index = index; - } - Interlocked.Decrement(ref _lock); + buffers[index] = array; + _index = index; } + Interlocked.Decrement(ref _lock); } } + } - private readonly Bucket _bucket4, _bucket8, _bucket16, _bucket32; + private readonly Bucket _bucket4, _bucket8, _bucket16, _bucket32; - public CustomArrayPool(int size4, int size8, int size16, int size32) - { - _bucket4 = new Bucket(size4); - _bucket8 = new Bucket(size8); - _bucket16 = new Bucket(size16); - _bucket32 = new Bucket(size32); - } + public CustomArrayPool(int size4, int size8, int size16, int size32) + { + _bucket4 = new Bucket(size4); + _bucket8 = new Bucket(size8); + _bucket16 = new Bucket(size16); + _bucket32 = new Bucket(size32); + } - private Bucket? SelectBucket(int length) + private Bucket? SelectBucket(int length) + { + return length switch { - return length switch - { - 4 => _bucket4, - 8 => _bucket8, - 16 => _bucket16, - 32 => _bucket32, - _ => null - }; - } + 4 => _bucket4, + 8 => _bucket8, + 16 => _bucket16, + 32 => _bucket32, + _ => null + }; + } - public T[] Rent(int length) - { - return SelectBucket(length)?.Rent() ?? new T[length]; - } + public T[] Rent(int length) + { + return SelectBucket(length)?.Rent() ?? new T[length]; + } - public void Return(T[] array) - { - SelectBucket(array.Length)?.Return(array); - } + public void Return(T[] array) + { + SelectBucket(array.Length)?.Return(array); } } diff --git a/src/Markdig/Helpers/DefaultObjectCache.cs b/src/Markdig/Helpers/DefaultObjectCache.cs index 18e3070e3..674892556 100644 --- a/src/Markdig/Helpers/DefaultObjectCache.cs +++ b/src/Markdig/Helpers/DefaultObjectCache.cs @@ -1,19 +1,18 @@ -// Copyright (c) Alexandre Mutel. All rights reserved. +// 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 Markdig.Helpers +namespace Markdig.Helpers; + +/// +/// A default object cache that expect the type {T} to provide a parameter less constructor +/// +/// The type of item to cache +/// +public abstract class DefaultObjectCache : ObjectCache where T : class, new() { - /// - /// A default object cache that expect the type {T} to provide a parameter less constructor - /// - /// The type of item to cache - /// - public abstract class DefaultObjectCache : ObjectCache where T : class, new() + protected override T NewInstance() { - protected override T NewInstance() - { - return new T(); - } + return new T(); } } \ No newline at end of file diff --git a/src/Markdig/Helpers/EntityHelper.cs b/src/Markdig/Helpers/EntityHelper.cs index afc9cc350..df6d2d8d2 100644 --- a/src/Markdig/Helpers/EntityHelper.cs +++ b/src/Markdig/Helpers/EntityHelper.cs @@ -31,2205 +31,2201 @@ // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -using System; -using System.Collections.Generic; +namespace Markdig.Helpers; -namespace Markdig.Helpers +/// +/// Helper class to decode an entity. +/// +public static class EntityHelper { /// - /// Helper class to decode an entity. + /// Decodes the given HTML entity to the matching Unicode characters. /// - public static class EntityHelper + /// The entity without & and ; symbols, for example, copy. + /// The unicode character set or null if the entity was not recognized. + public static string? DecodeEntity(ReadOnlySpan entity) { - /// - /// Decodes the given HTML entity to the matching Unicode characters. - /// - /// The entity without & and ; symbols, for example, copy. - /// The unicode character set or null if the entity was not recognized. - public static string? DecodeEntity(ReadOnlySpan entity) - { - if (EntityMap.TryMatchExact(entity, out KeyValuePair result)) - return result.Value; + if (EntityMap.TryMatchExact(entity, out KeyValuePair result)) + return result.Value; - return null; - } + return null; + } - /// - /// Decodes the given UTF-32 character code to the matching set of UTF-16 characters. - /// - /// The unicode character set or null if the entity was not recognized. - public static string DecodeEntity(int utf32) - { - if (!CharHelper.IsInInclusiveRange(utf32, 1, 1114111) || CharHelper.IsInInclusiveRange(utf32, 55296, 57343)) - return CharHelper.ReplacementCharString; + /// + /// Decodes the given UTF-32 character code to the matching set of UTF-16 characters. + /// + /// The unicode character set or null if the entity was not recognized. + public static string DecodeEntity(int utf32) + { + if (!CharHelper.IsInInclusiveRange(utf32, 1, 1114111) || CharHelper.IsInInclusiveRange(utf32, 55296, 57343)) + return CharHelper.ReplacementCharString; - if (utf32 < 65536) - return char.ToString((char)utf32); + if (utf32 < 65536) + return char.ToString((char)utf32); - utf32 -= 65536; - return new string( + utf32 -= 65536; + return new string( #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER - stackalloc + stackalloc #else - new + new #endif - char[] - { - (char)((uint)utf32 / 1024 + 55296), - (char)((uint)utf32 % 1024 + 56320) - }); - } + char[] + { + (char)((uint)utf32 / 1024 + 55296), + (char)((uint)utf32 % 1024 + 56320) + }); + } - internal static void DecodeEntity(int utf32, ref ValueStringBuilder sb) + internal static void DecodeEntity(int utf32, ref ValueStringBuilder sb) + { + if (!CharHelper.IsInInclusiveRange(utf32, 1, 1114111) || CharHelper.IsInInclusiveRange(utf32, 55296, 57343)) { - if (!CharHelper.IsInInclusiveRange(utf32, 1, 1114111) || CharHelper.IsInInclusiveRange(utf32, 55296, 57343)) - { - sb.Append(CharHelper.ReplacementChar); - } - else if (utf32 < 65536) - { - sb.Append((char)utf32); - } - else - { - utf32 -= 65536; - sb.Append((char)((uint)utf32 / 1024 + 55296)); - sb.Append((char)((uint)utf32 % 1024 + 56320)); - } + sb.Append(CharHelper.ReplacementChar); } - - #region [ EntityMap ] - /// - /// Source: http://www.w3.org/html/wg/drafts/html/master/syntax.html#named-character-references - /// - private static readonly CompactPrefixTree EntityMap = new CompactPrefixTree(2125, 3385, 3510) + else if (utf32 < 65536) { - { "Aacute", "\u00C1" }, - { "aacute", "\u00E1" }, - { "Abreve", "\u0102" }, - { "abreve", "\u0103" }, - { "ac", "\u223E" }, - { "acd", "\u223F" }, - { "acE", "\u223E\u0333" }, - { "Acirc", "\u00C2" }, - { "acirc", "\u00E2" }, - { "acute", "\u00B4" }, - { "Acy", "\u0410" }, - { "acy", "\u0430" }, - { "AElig", "\u00C6" }, - { "aelig", "\u00E6" }, - { "af", "\u2061" }, - { "Afr", "\uD835\uDD04" }, - { "afr", "\uD835\uDD1E" }, - { "Agrave", "\u00C0" }, - { "agrave", "\u00E0" }, - { "alefsym", "\u2135" }, - { "aleph", "\u2135" }, - { "Alpha", "\u0391" }, - { "alpha", "\u03B1" }, - { "Amacr", "\u0100" }, - { "amacr", "\u0101" }, - { "amalg", "\u2A3F" }, - { "AMP", "\u0026" }, - { "amp", "\u0026" }, - { "And", "\u2A53" }, - { "and", "\u2227" }, - { "andand", "\u2A55" }, - { "andd", "\u2A5C" }, - { "andslope", "\u2A58" }, - { "andv", "\u2A5A" }, - { "ang", "\u2220" }, - { "ange", "\u29A4" }, - { "angle", "\u2220" }, - { "angmsd", "\u2221" }, - { "angmsdaa", "\u29A8" }, - { "angmsdab", "\u29A9" }, - { "angmsdac", "\u29AA" }, - { "angmsdad", "\u29AB" }, - { "angmsdae", "\u29AC" }, - { "angmsdaf", "\u29AD" }, - { "angmsdag", "\u29AE" }, - { "angmsdah", "\u29AF" }, - { "angrt", "\u221F" }, - { "angrtvb", "\u22BE" }, - { "angrtvbd", "\u299D" }, - { "angsph", "\u2222" }, - { "angst", "\u00C5" }, - { "angzarr", "\u237C" }, - { "Aogon", "\u0104" }, - { "aogon", "\u0105" }, - { "Aopf", "\uD835\uDD38" }, - { "aopf", "\uD835\uDD52" }, - { "ap", "\u2248" }, - { "apacir", "\u2A6F" }, - { "apE", "\u2A70" }, - { "ape", "\u224A" }, - { "apid", "\u224B" }, - { "apos", "\u0027" }, - { "ApplyFunction", "\u2061" }, - { "approx", "\u2248" }, - { "approxeq", "\u224A" }, - { "Aring", "\u00C5" }, - { "aring", "\u00E5" }, - { "Ascr", "\uD835\uDC9C" }, - { "ascr", "\uD835\uDCB6" }, - { "Assign", "\u2254" }, - { "ast", "\u002A" }, - { "asymp", "\u2248" }, - { "asympeq", "\u224D" }, - { "Atilde", "\u00C3" }, - { "atilde", "\u00E3" }, - { "Auml", "\u00C4" }, - { "auml", "\u00E4" }, - { "awconint", "\u2233" }, - { "awint", "\u2A11" }, - { "backcong", "\u224C" }, - { "backepsilon", "\u03F6" }, - { "backprime", "\u2035" }, - { "backsim", "\u223D" }, - { "backsimeq", "\u22CD" }, - { "Backslash", "\u2216" }, - { "Barv", "\u2AE7" }, - { "barvee", "\u22BD" }, - { "Barwed", "\u2306" }, - { "barwed", "\u2305" }, - { "barwedge", "\u2305" }, - { "bbrk", "\u23B5" }, - { "bbrktbrk", "\u23B6" }, - { "bcong", "\u224C" }, - { "Bcy", "\u0411" }, - { "bcy", "\u0431" }, - { "bdquo", "\u201E" }, - { "becaus", "\u2235" }, - { "Because", "\u2235" }, - { "because", "\u2235" }, - { "bemptyv", "\u29B0" }, - { "bepsi", "\u03F6" }, - { "bernou", "\u212C" }, - { "Bernoullis", "\u212C" }, - { "Beta", "\u0392" }, - { "beta", "\u03B2" }, - { "beth", "\u2136" }, - { "between", "\u226C" }, - { "Bfr", "\uD835\uDD05" }, - { "bfr", "\uD835\uDD1F" }, - { "bigcap", "\u22C2" }, - { "bigcirc", "\u25EF" }, - { "bigcup", "\u22C3" }, - { "bigodot", "\u2A00" }, - { "bigoplus", "\u2A01" }, - { "bigotimes", "\u2A02" }, - { "bigsqcup", "\u2A06" }, - { "bigstar", "\u2605" }, - { "bigtriangledown", "\u25BD" }, - { "bigtriangleup", "\u25B3" }, - { "biguplus", "\u2A04" }, - { "bigvee", "\u22C1" }, - { "bigwedge", "\u22C0" }, - { "bkarow", "\u290D" }, - { "blacklozenge", "\u29EB" }, - { "blacksquare", "\u25AA" }, - { "blacktriangle", "\u25B4" }, - { "blacktriangledown", "\u25BE" }, - { "blacktriangleleft", "\u25C2" }, - { "blacktriangleright", "\u25B8" }, - { "blank", "\u2423" }, - { "blk12", "\u2592" }, - { "blk14", "\u2591" }, - { "blk34", "\u2593" }, - { "block", "\u2588" }, - { "bne", "\u003D\u20E5" }, - { "bnequiv", "\u2261\u20E5" }, - { "bNot", "\u2AED" }, - { "bnot", "\u2310" }, - { "Bopf", "\uD835\uDD39" }, - { "bopf", "\uD835\uDD53" }, - { "bot", "\u22A5" }, - { "bottom", "\u22A5" }, - { "bowtie", "\u22C8" }, - { "boxbox", "\u29C9" }, - { "boxDL", "\u2557" }, - { "boxDl", "\u2556" }, - { "boxdL", "\u2555" }, - { "boxdl", "\u2510" }, - { "boxDR", "\u2554" }, - { "boxDr", "\u2553" }, - { "boxdR", "\u2552" }, - { "boxdr", "\u250C" }, - { "boxH", "\u2550" }, - { "boxh", "\u2500" }, - { "boxHD", "\u2566" }, - { "boxHd", "\u2564" }, - { "boxhD", "\u2565" }, - { "boxhd", "\u252C" }, - { "boxHU", "\u2569" }, - { "boxHu", "\u2567" }, - { "boxhU", "\u2568" }, - { "boxhu", "\u2534" }, - { "boxminus", "\u229F" }, - { "boxplus", "\u229E" }, - { "boxtimes", "\u22A0" }, - { "boxUL", "\u255D" }, - { "boxUl", "\u255C" }, - { "boxuL", "\u255B" }, - { "boxul", "\u2518" }, - { "boxUR", "\u255A" }, - { "boxUr", "\u2559" }, - { "boxuR", "\u2558" }, - { "boxur", "\u2514" }, - { "boxV", "\u2551" }, - { "boxv", "\u2502" }, - { "boxVH", "\u256C" }, - { "boxVh", "\u256B" }, - { "boxvH", "\u256A" }, - { "boxvh", "\u253C" }, - { "boxVL", "\u2563" }, - { "boxVl", "\u2562" }, - { "boxvL", "\u2561" }, - { "boxvl", "\u2524" }, - { "boxVR", "\u2560" }, - { "boxVr", "\u255F" }, - { "boxvR", "\u255E" }, - { "boxvr", "\u251C" }, - { "bprime", "\u2035" }, - { "Breve", "\u02D8" }, - { "breve", "\u02D8" }, - { "brvbar", "\u00A6" }, - { "Bscr", "\u212C" }, - { "bscr", "\uD835\uDCB7" }, - { "bsemi", "\u204F" }, - { "bsim", "\u223D" }, - { "bsime", "\u22CD" }, - { "bsol", "\u005C" }, - { "bsolb", "\u29C5" }, - { "bsolhsub", "\u27C8" }, - { "bull", "\u2022" }, - { "bullet", "\u2022" }, - { "bump", "\u224E" }, - { "bumpE", "\u2AAE" }, - { "bumpe", "\u224F" }, - { "Bumpeq", "\u224E" }, - { "bumpeq", "\u224F" }, - { "Cacute", "\u0106" }, - { "cacute", "\u0107" }, - { "Cap", "\u22D2" }, - { "cap", "\u2229" }, - { "capand", "\u2A44" }, - { "capbrcup", "\u2A49" }, - { "capcap", "\u2A4B" }, - { "capcup", "\u2A47" }, - { "capdot", "\u2A40" }, - { "CapitalDifferentialD", "\u2145" }, - { "caps", "\u2229\uFE00" }, - { "caret", "\u2041" }, - { "caron", "\u02C7" }, - { "Cayleys", "\u212D" }, - { "ccaps", "\u2A4D" }, - { "Ccaron", "\u010C" }, - { "ccaron", "\u010D" }, - { "Ccedil", "\u00C7" }, - { "ccedil", "\u00E7" }, - { "Ccirc", "\u0108" }, - { "ccirc", "\u0109" }, - { "Cconint", "\u2230" }, - { "ccups", "\u2A4C" }, - { "ccupssm", "\u2A50" }, - { "Cdot", "\u010A" }, - { "cdot", "\u010B" }, - { "cedil", "\u00B8" }, - { "Cedilla", "\u00B8" }, - { "cemptyv", "\u29B2" }, - { "cent", "\u00A2" }, - { "CenterDot", "\u00B7" }, - { "centerdot", "\u00B7" }, - { "Cfr", "\u212D" }, - { "cfr", "\uD835\uDD20" }, - { "CHcy", "\u0427" }, - { "chcy", "\u0447" }, - { "check", "\u2713" }, - { "checkmark", "\u2713" }, - { "Chi", "\u03A7" }, - { "chi", "\u03C7" }, - { "cir", "\u25CB" }, - { "circ", "\u02C6" }, - { "circeq", "\u2257" }, - { "circlearrowleft", "\u21BA" }, - { "circlearrowright", "\u21BB" }, - { "circledast", "\u229B" }, - { "circledcirc", "\u229A" }, - { "circleddash", "\u229D" }, - { "CircleDot", "\u2299" }, - { "circledR", "\u00AE" }, - { "circledS", "\u24C8" }, - { "CircleMinus", "\u2296" }, - { "CirclePlus", "\u2295" }, - { "CircleTimes", "\u2297" }, - { "cirE", "\u29C3" }, - { "cire", "\u2257" }, - { "cirfnint", "\u2A10" }, - { "cirmid", "\u2AEF" }, - { "cirscir", "\u29C2" }, - { "ClockwiseContourIntegral", "\u2232" }, - { "CloseCurlyDoubleQuote", "\u201D" }, - { "CloseCurlyQuote", "\u2019" }, - { "clubs", "\u2663" }, - { "clubsuit", "\u2663" }, - { "Colon", "\u2237" }, - { "colon", "\u003A" }, - { "Colone", "\u2A74" }, - { "colone", "\u2254" }, - { "coloneq", "\u2254" }, - { "comma", "\u002C" }, - { "commat", "\u0040" }, - { "comp", "\u2201" }, - { "compfn", "\u2218" }, - { "complement", "\u2201" }, - { "complexes", "\u2102" }, - { "cong", "\u2245" }, - { "congdot", "\u2A6D" }, - { "Congruent", "\u2261" }, - { "Conint", "\u222F" }, - { "conint", "\u222E" }, - { "ContourIntegral", "\u222E" }, - { "Copf", "\u2102" }, - { "copf", "\uD835\uDD54" }, - { "coprod", "\u2210" }, - { "Coproduct", "\u2210" }, - { "COPY", "\u00A9" }, - { "copy", "\u00A9" }, - { "copysr", "\u2117" }, - { "CounterClockwiseContourIntegral", "\u2233" }, - { "crarr", "\u21B5" }, - { "Cross", "\u2A2F" }, - { "cross", "\u2717" }, - { "Cscr", "\uD835\uDC9E" }, - { "cscr", "\uD835\uDCB8" }, - { "csub", "\u2ACF" }, - { "csube", "\u2AD1" }, - { "csup", "\u2AD0" }, - { "csupe", "\u2AD2" }, - { "ctdot", "\u22EF" }, - { "cudarrl", "\u2938" }, - { "cudarrr", "\u2935" }, - { "cuepr", "\u22DE" }, - { "cuesc", "\u22DF" }, - { "cularr", "\u21B6" }, - { "cularrp", "\u293D" }, - { "Cup", "\u22D3" }, - { "cup", "\u222A" }, - { "cupbrcap", "\u2A48" }, - { "CupCap", "\u224D" }, - { "cupcap", "\u2A46" }, - { "cupcup", "\u2A4A" }, - { "cupdot", "\u228D" }, - { "cupor", "\u2A45" }, - { "cups", "\u222A\uFE00" }, - { "curarr", "\u21B7" }, - { "curarrm", "\u293C" }, - { "curlyeqprec", "\u22DE" }, - { "curlyeqsucc", "\u22DF" }, - { "curlyvee", "\u22CE" }, - { "curlywedge", "\u22CF" }, - { "curren", "\u00A4" }, - { "curvearrowleft", "\u21B6" }, - { "curvearrowright", "\u21B7" }, - { "cuvee", "\u22CE" }, - { "cuwed", "\u22CF" }, - { "cwconint", "\u2232" }, - { "cwint", "\u2231" }, - { "cylcty", "\u232D" }, - { "Dagger", "\u2021" }, - { "dagger", "\u2020" }, - { "daleth", "\u2138" }, - { "Darr", "\u21A1" }, - { "dArr", "\u21D3" }, - { "darr", "\u2193" }, - { "dash", "\u2010" }, - { "Dashv", "\u2AE4" }, - { "dashv", "\u22A3" }, - { "dbkarow", "\u290F" }, - { "dblac", "\u02DD" }, - { "Dcaron", "\u010E" }, - { "dcaron", "\u010F" }, - { "Dcy", "\u0414" }, - { "dcy", "\u0434" }, - { "DD", "\u2145" }, - { "dd", "\u2146" }, - { "ddagger", "\u2021" }, - { "ddarr", "\u21CA" }, - { "DDotrahd", "\u2911" }, - { "ddotseq", "\u2A77" }, - { "deg", "\u00B0" }, - { "Del", "\u2207" }, - { "Delta", "\u0394" }, - { "delta", "\u03B4" }, - { "demptyv", "\u29B1" }, - { "dfisht", "\u297F" }, - { "Dfr", "\uD835\uDD07" }, - { "dfr", "\uD835\uDD21" }, - { "dHar", "\u2965" }, - { "dharl", "\u21C3" }, - { "dharr", "\u21C2" }, - { "DiacriticalAcute", "\u00B4" }, - { "DiacriticalDot", "\u02D9" }, - { "DiacriticalDoubleAcute", "\u02DD" }, - { "DiacriticalGrave", "\u0060" }, - { "DiacriticalTilde", "\u02DC" }, - { "diam", "\u22C4" }, - { "Diamond", "\u22C4" }, - { "diamond", "\u22C4" }, - { "diamondsuit", "\u2666" }, - { "diams", "\u2666" }, - { "die", "\u00A8" }, - { "DifferentialD", "\u2146" }, - { "digamma", "\u03DD" }, - { "disin", "\u22F2" }, - { "div", "\u00F7" }, - { "divide", "\u00F7" }, - { "divideontimes", "\u22C7" }, - { "divonx", "\u22C7" }, - { "DJcy", "\u0402" }, - { "djcy", "\u0452" }, - { "dlcorn", "\u231E" }, - { "dlcrop", "\u230D" }, - { "dollar", "\u0024" }, - { "Dopf", "\uD835\uDD3B" }, - { "dopf", "\uD835\uDD55" }, - { "Dot", "\u00A8" }, - { "dot", "\u02D9" }, - { "DotDot", "\u20DC" }, - { "doteq", "\u2250" }, - { "doteqdot", "\u2251" }, - { "DotEqual", "\u2250" }, - { "dotminus", "\u2238" }, - { "dotplus", "\u2214" }, - { "dotsquare", "\u22A1" }, - { "doublebarwedge", "\u2306" }, - { "DoubleContourIntegral", "\u222F" }, - { "DoubleDot", "\u00A8" }, - { "DoubleDownArrow", "\u21D3" }, - { "DoubleLeftArrow", "\u21D0" }, - { "DoubleLeftRightArrow", "\u21D4" }, - { "DoubleLeftTee", "\u2AE4" }, - { "DoubleLongLeftArrow", "\u27F8" }, - { "DoubleLongLeftRightArrow", "\u27FA" }, - { "DoubleLongRightArrow", "\u27F9" }, - { "DoubleRightArrow", "\u21D2" }, - { "DoubleRightTee", "\u22A8" }, - { "DoubleUpArrow", "\u21D1" }, - { "DoubleUpDownArrow", "\u21D5" }, - { "DoubleVerticalBar", "\u2225" }, - { "DownArrow", "\u2193" }, - { "Downarrow", "\u21D3" }, - { "downarrow", "\u2193" }, - { "DownArrowBar", "\u2913" }, - { "DownArrowUpArrow", "\u21F5" }, - { "DownBreve", "\u0311" }, - { "downdownarrows", "\u21CA" }, - { "downharpoonleft", "\u21C3" }, - { "downharpoonright", "\u21C2" }, - { "DownLeftRightVector", "\u2950" }, - { "DownLeftTeeVector", "\u295E" }, - { "DownLeftVector", "\u21BD" }, - { "DownLeftVectorBar", "\u2956" }, - { "DownRightTeeVector", "\u295F" }, - { "DownRightVector", "\u21C1" }, - { "DownRightVectorBar", "\u2957" }, - { "DownTee", "\u22A4" }, - { "DownTeeArrow", "\u21A7" }, - { "drbkarow", "\u2910" }, - { "drcorn", "\u231F" }, - { "drcrop", "\u230C" }, - { "Dscr", "\uD835\uDC9F" }, - { "dscr", "\uD835\uDCB9" }, - { "DScy", "\u0405" }, - { "dscy", "\u0455" }, - { "dsol", "\u29F6" }, - { "Dstrok", "\u0110" }, - { "dstrok", "\u0111" }, - { "dtdot", "\u22F1" }, - { "dtri", "\u25BF" }, - { "dtrif", "\u25BE" }, - { "duarr", "\u21F5" }, - { "duhar", "\u296F" }, - { "dwangle", "\u29A6" }, - { "DZcy", "\u040F" }, - { "dzcy", "\u045F" }, - { "dzigrarr", "\u27FF" }, - { "Eacute", "\u00C9" }, - { "eacute", "\u00E9" }, - { "easter", "\u2A6E" }, - { "Ecaron", "\u011A" }, - { "ecaron", "\u011B" }, - { "ecir", "\u2256" }, - { "Ecirc", "\u00CA" }, - { "ecirc", "\u00EA" }, - { "ecolon", "\u2255" }, - { "Ecy", "\u042D" }, - { "ecy", "\u044D" }, - { "eDDot", "\u2A77" }, - { "Edot", "\u0116" }, - { "eDot", "\u2251" }, - { "edot", "\u0117" }, - { "ee", "\u2147" }, - { "efDot", "\u2252" }, - { "Efr", "\uD835\uDD08" }, - { "efr", "\uD835\uDD22" }, - { "eg", "\u2A9A" }, - { "Egrave", "\u00C8" }, - { "egrave", "\u00E8" }, - { "egs", "\u2A96" }, - { "egsdot", "\u2A98" }, - { "el", "\u2A99" }, - { "Element", "\u2208" }, - { "elinters", "\u23E7" }, - { "ell", "\u2113" }, - { "els", "\u2A95" }, - { "elsdot", "\u2A97" }, - { "Emacr", "\u0112" }, - { "emacr", "\u0113" }, - { "empty", "\u2205" }, - { "emptyset", "\u2205" }, - { "EmptySmallSquare", "\u25FB" }, - { "emptyv", "\u2205" }, - { "EmptyVerySmallSquare", "\u25AB" }, - { "emsp", "\u2003" }, - { "emsp13", "\u2004" }, - { "emsp14", "\u2005" }, - { "ENG", "\u014A" }, - { "eng", "\u014B" }, - { "ensp", "\u2002" }, - { "Eogon", "\u0118" }, - { "eogon", "\u0119" }, - { "Eopf", "\uD835\uDD3C" }, - { "eopf", "\uD835\uDD56" }, - { "epar", "\u22D5" }, - { "eparsl", "\u29E3" }, - { "eplus", "\u2A71" }, - { "epsi", "\u03B5" }, - { "Epsilon", "\u0395" }, - { "epsilon", "\u03B5" }, - { "epsiv", "\u03F5" }, - { "eqcirc", "\u2256" }, - { "eqcolon", "\u2255" }, - { "eqsim", "\u2242" }, - { "eqslantgtr", "\u2A96" }, - { "eqslantless", "\u2A95" }, - { "Equal", "\u2A75" }, - { "equals", "\u003D" }, - { "EqualTilde", "\u2242" }, - { "equest", "\u225F" }, - { "Equilibrium", "\u21CC" }, - { "equiv", "\u2261" }, - { "equivDD", "\u2A78" }, - { "eqvparsl", "\u29E5" }, - { "erarr", "\u2971" }, - { "erDot", "\u2253" }, - { "Escr", "\u2130" }, - { "escr", "\u212F" }, - { "esdot", "\u2250" }, - { "Esim", "\u2A73" }, - { "esim", "\u2242" }, - { "Eta", "\u0397" }, - { "eta", "\u03B7" }, - { "ETH", "\u00D0" }, - { "eth", "\u00F0" }, - { "Euml", "\u00CB" }, - { "euml", "\u00EB" }, - { "euro", "\u20AC" }, - { "excl", "\u0021" }, - { "exist", "\u2203" }, - { "Exists", "\u2203" }, - { "expectation", "\u2130" }, - { "ExponentialE", "\u2147" }, - { "exponentiale", "\u2147" }, - { "fallingdotseq", "\u2252" }, - { "Fcy", "\u0424" }, - { "fcy", "\u0444" }, - { "female", "\u2640" }, - { "ffilig", "\uFB03" }, - { "fflig", "\uFB00" }, - { "ffllig", "\uFB04" }, - { "Ffr", "\uD835\uDD09" }, - { "ffr", "\uD835\uDD23" }, - { "filig", "\uFB01" }, - { "FilledSmallSquare", "\u25FC" }, - { "FilledVerySmallSquare", "\u25AA" }, - { "fjlig", "\u0066\u006A" }, - { "flat", "\u266D" }, - { "fllig", "\uFB02" }, - { "fltns", "\u25B1" }, - { "fnof", "\u0192" }, - { "Fopf", "\uD835\uDD3D" }, - { "fopf", "\uD835\uDD57" }, - { "ForAll", "\u2200" }, - { "forall", "\u2200" }, - { "fork", "\u22D4" }, - { "forkv", "\u2AD9" }, - { "Fouriertrf", "\u2131" }, - { "fpartint", "\u2A0D" }, - { "frac12", "\u00BD" }, - { "frac13", "\u2153" }, - { "frac14", "\u00BC" }, - { "frac15", "\u2155" }, - { "frac16", "\u2159" }, - { "frac18", "\u215B" }, - { "frac23", "\u2154" }, - { "frac25", "\u2156" }, - { "frac34", "\u00BE" }, - { "frac35", "\u2157" }, - { "frac38", "\u215C" }, - { "frac45", "\u2158" }, - { "frac56", "\u215A" }, - { "frac58", "\u215D" }, - { "frac78", "\u215E" }, - { "frasl", "\u2044" }, - { "frown", "\u2322" }, - { "Fscr", "\u2131" }, - { "fscr", "\uD835\uDCBB" }, - { "gacute", "\u01F5" }, - { "Gamma", "\u0393" }, - { "gamma", "\u03B3" }, - { "Gammad", "\u03DC" }, - { "gammad", "\u03DD" }, - { "gap", "\u2A86" }, - { "Gbreve", "\u011E" }, - { "gbreve", "\u011F" }, - { "Gcedil", "\u0122" }, - { "Gcirc", "\u011C" }, - { "gcirc", "\u011D" }, - { "Gcy", "\u0413" }, - { "gcy", "\u0433" }, - { "Gdot", "\u0120" }, - { "gdot", "\u0121" }, - { "gE", "\u2267" }, - { "ge", "\u2265" }, - { "gEl", "\u2A8C" }, - { "gel", "\u22DB" }, - { "geq", "\u2265" }, - { "geqq", "\u2267" }, - { "geqslant", "\u2A7E" }, - { "ges", "\u2A7E" }, - { "gescc", "\u2AA9" }, - { "gesdot", "\u2A80" }, - { "gesdoto", "\u2A82" }, - { "gesdotol", "\u2A84" }, - { "gesl", "\u22DB\uFE00" }, - { "gesles", "\u2A94" }, - { "Gfr", "\uD835\uDD0A" }, - { "gfr", "\uD835\uDD24" }, - { "Gg", "\u22D9" }, - { "gg", "\u226B" }, - { "ggg", "\u22D9" }, - { "gimel", "\u2137" }, - { "GJcy", "\u0403" }, - { "gjcy", "\u0453" }, - { "gl", "\u2277" }, - { "gla", "\u2AA5" }, - { "glE", "\u2A92" }, - { "glj", "\u2AA4" }, - { "gnap", "\u2A8A" }, - { "gnapprox", "\u2A8A" }, - { "gnE", "\u2269" }, - { "gne", "\u2A88" }, - { "gneq", "\u2A88" }, - { "gneqq", "\u2269" }, - { "gnsim", "\u22E7" }, - { "Gopf", "\uD835\uDD3E" }, - { "gopf", "\uD835\uDD58" }, - { "grave", "\u0060" }, - { "GreaterEqual", "\u2265" }, - { "GreaterEqualLess", "\u22DB" }, - { "GreaterFullEqual", "\u2267" }, - { "GreaterGreater", "\u2AA2" }, - { "GreaterLess", "\u2277" }, - { "GreaterSlantEqual", "\u2A7E" }, - { "GreaterTilde", "\u2273" }, - { "Gscr", "\uD835\uDCA2" }, - { "gscr", "\u210A" }, - { "gsim", "\u2273" }, - { "gsime", "\u2A8E" }, - { "gsiml", "\u2A90" }, - { "GT", "\u003E" }, - { "Gt", "\u226B" }, - { "gt", "\u003E" }, - { "gtcc", "\u2AA7" }, - { "gtcir", "\u2A7A" }, - { "gtdot", "\u22D7" }, - { "gtlPar", "\u2995" }, - { "gtquest", "\u2A7C" }, - { "gtrapprox", "\u2A86" }, - { "gtrarr", "\u2978" }, - { "gtrdot", "\u22D7" }, - { "gtreqless", "\u22DB" }, - { "gtreqqless", "\u2A8C" }, - { "gtrless", "\u2277" }, - { "gtrsim", "\u2273" }, - { "gvertneqq", "\u2269\uFE00" }, - { "gvnE", "\u2269\uFE00" }, - { "Hacek", "\u02C7" }, - { "hairsp", "\u200A" }, - { "half", "\u00BD" }, - { "hamilt", "\u210B" }, - { "HARDcy", "\u042A" }, - { "hardcy", "\u044A" }, - { "hArr", "\u21D4" }, - { "harr", "\u2194" }, - { "harrcir", "\u2948" }, - { "harrw", "\u21AD" }, - { "Hat", "\u005E" }, - { "hbar", "\u210F" }, - { "Hcirc", "\u0124" }, - { "hcirc", "\u0125" }, - { "hearts", "\u2665" }, - { "heartsuit", "\u2665" }, - { "hellip", "\u2026" }, - { "hercon", "\u22B9" }, - { "Hfr", "\u210C" }, - { "hfr", "\uD835\uDD25" }, - { "HilbertSpace", "\u210B" }, - { "hksearow", "\u2925" }, - { "hkswarow", "\u2926" }, - { "hoarr", "\u21FF" }, - { "homtht", "\u223B" }, - { "hookleftarrow", "\u21A9" }, - { "hookrightarrow", "\u21AA" }, - { "Hopf", "\u210D" }, - { "hopf", "\uD835\uDD59" }, - { "horbar", "\u2015" }, - { "HorizontalLine", "\u2500" }, - { "Hscr", "\u210B" }, - { "hscr", "\uD835\uDCBD" }, - { "hslash", "\u210F" }, - { "Hstrok", "\u0126" }, - { "hstrok", "\u0127" }, - { "HumpDownHump", "\u224E" }, - { "HumpEqual", "\u224F" }, - { "hybull", "\u2043" }, - { "hyphen", "\u2010" }, - { "Iacute", "\u00CD" }, - { "iacute", "\u00ED" }, - { "ic", "\u2063" }, - { "Icirc", "\u00CE" }, - { "icirc", "\u00EE" }, - { "Icy", "\u0418" }, - { "icy", "\u0438" }, - { "Idot", "\u0130" }, - { "IEcy", "\u0415" }, - { "iecy", "\u0435" }, - { "iexcl", "\u00A1" }, - { "iff", "\u21D4" }, - { "Ifr", "\u2111" }, - { "ifr", "\uD835\uDD26" }, - { "Igrave", "\u00CC" }, - { "igrave", "\u00EC" }, - { "ii", "\u2148" }, - { "iiiint", "\u2A0C" }, - { "iiint", "\u222D" }, - { "iinfin", "\u29DC" }, - { "iiota", "\u2129" }, - { "IJlig", "\u0132" }, - { "ijlig", "\u0133" }, - { "Im", "\u2111" }, - { "Imacr", "\u012A" }, - { "imacr", "\u012B" }, - { "image", "\u2111" }, - { "ImaginaryI", "\u2148" }, - { "imagline", "\u2110" }, - { "imagpart", "\u2111" }, - { "imath", "\u0131" }, - { "imof", "\u22B7" }, - { "imped", "\u01B5" }, - { "Implies", "\u21D2" }, - { "in", "\u2208" }, - { "incare", "\u2105" }, - { "infin", "\u221E" }, - { "infintie", "\u29DD" }, - { "inodot", "\u0131" }, - { "Int", "\u222C" }, - { "int", "\u222B" }, - { "intcal", "\u22BA" }, - { "integers", "\u2124" }, - { "Integral", "\u222B" }, - { "intercal", "\u22BA" }, - { "Intersection", "\u22C2" }, - { "intlarhk", "\u2A17" }, - { "intprod", "\u2A3C" }, - { "InvisibleComma", "\u2063" }, - { "InvisibleTimes", "\u2062" }, - { "IOcy", "\u0401" }, - { "iocy", "\u0451" }, - { "Iogon", "\u012E" }, - { "iogon", "\u012F" }, - { "Iopf", "\uD835\uDD40" }, - { "iopf", "\uD835\uDD5A" }, - { "Iota", "\u0399" }, - { "iota", "\u03B9" }, - { "iprod", "\u2A3C" }, - { "iquest", "\u00BF" }, - { "Iscr", "\u2110" }, - { "iscr", "\uD835\uDCBE" }, - { "isin", "\u2208" }, - { "isindot", "\u22F5" }, - { "isinE", "\u22F9" }, - { "isins", "\u22F4" }, - { "isinsv", "\u22F3" }, - { "isinv", "\u2208" }, - { "it", "\u2062" }, - { "Itilde", "\u0128" }, - { "itilde", "\u0129" }, - { "Iukcy", "\u0406" }, - { "iukcy", "\u0456" }, - { "Iuml", "\u00CF" }, - { "iuml", "\u00EF" }, - { "Jcirc", "\u0134" }, - { "jcirc", "\u0135" }, - { "Jcy", "\u0419" }, - { "jcy", "\u0439" }, - { "Jfr", "\uD835\uDD0D" }, - { "jfr", "\uD835\uDD27" }, - { "jmath", "\u0237" }, - { "Jopf", "\uD835\uDD41" }, - { "jopf", "\uD835\uDD5B" }, - { "Jscr", "\uD835\uDCA5" }, - { "jscr", "\uD835\uDCBF" }, - { "Jsercy", "\u0408" }, - { "jsercy", "\u0458" }, - { "Jukcy", "\u0404" }, - { "jukcy", "\u0454" }, - { "Kappa", "\u039A" }, - { "kappa", "\u03BA" }, - { "kappav", "\u03F0" }, - { "Kcedil", "\u0136" }, - { "kcedil", "\u0137" }, - { "Kcy", "\u041A" }, - { "kcy", "\u043A" }, - { "Kfr", "\uD835\uDD0E" }, - { "kfr", "\uD835\uDD28" }, - { "kgreen", "\u0138" }, - { "KHcy", "\u0425" }, - { "khcy", "\u0445" }, - { "KJcy", "\u040C" }, - { "kjcy", "\u045C" }, - { "Kopf", "\uD835\uDD42" }, - { "kopf", "\uD835\uDD5C" }, - { "Kscr", "\uD835\uDCA6" }, - { "kscr", "\uD835\uDCC0" }, - { "lAarr", "\u21DA" }, - { "Lacute", "\u0139" }, - { "lacute", "\u013A" }, - { "laemptyv", "\u29B4" }, - { "lagran", "\u2112" }, - { "Lambda", "\u039B" }, - { "lambda", "\u03BB" }, - { "Lang", "\u27EA" }, - { "lang", "\u27E8" }, - { "langd", "\u2991" }, - { "langle", "\u27E8" }, - { "lap", "\u2A85" }, - { "Laplacetrf", "\u2112" }, - { "laquo", "\u00AB" }, - { "Larr", "\u219E" }, - { "lArr", "\u21D0" }, - { "larr", "\u2190" }, - { "larrb", "\u21E4" }, - { "larrbfs", "\u291F" }, - { "larrfs", "\u291D" }, - { "larrhk", "\u21A9" }, - { "larrlp", "\u21AB" }, - { "larrpl", "\u2939" }, - { "larrsim", "\u2973" }, - { "larrtl", "\u21A2" }, - { "lat", "\u2AAB" }, - { "lAtail", "\u291B" }, - { "latail", "\u2919" }, - { "late", "\u2AAD" }, - { "lates", "\u2AAD\uFE00" }, - { "lBarr", "\u290E" }, - { "lbarr", "\u290C" }, - { "lbbrk", "\u2772" }, - { "lbrace", "\u007B" }, - { "lbrack", "\u005B" }, - { "lbrke", "\u298B" }, - { "lbrksld", "\u298F" }, - { "lbrkslu", "\u298D" }, - { "Lcaron", "\u013D" }, - { "lcaron", "\u013E" }, - { "Lcedil", "\u013B" }, - { "lcedil", "\u013C" }, - { "lceil", "\u2308" }, - { "lcub", "\u007B" }, - { "Lcy", "\u041B" }, - { "lcy", "\u043B" }, - { "ldca", "\u2936" }, - { "ldquo", "\u201C" }, - { "ldquor", "\u201E" }, - { "ldrdhar", "\u2967" }, - { "ldrushar", "\u294B" }, - { "ldsh", "\u21B2" }, - { "lE", "\u2266" }, - { "le", "\u2264" }, - { "LeftAngleBracket", "\u27E8" }, - { "LeftArrow", "\u2190" }, - { "Leftarrow", "\u21D0" }, - { "leftarrow", "\u2190" }, - { "LeftArrowBar", "\u21E4" }, - { "LeftArrowRightArrow", "\u21C6" }, - { "leftarrowtail", "\u21A2" }, - { "LeftCeiling", "\u2308" }, - { "LeftDoubleBracket", "\u27E6" }, - { "LeftDownTeeVector", "\u2961" }, - { "LeftDownVector", "\u21C3" }, - { "LeftDownVectorBar", "\u2959" }, - { "LeftFloor", "\u230A" }, - { "leftharpoondown", "\u21BD" }, - { "leftharpoonup", "\u21BC" }, - { "leftleftarrows", "\u21C7" }, - { "LeftRightArrow", "\u2194" }, - { "Leftrightarrow", "\u21D4" }, - { "leftrightarrow", "\u2194" }, - { "leftrightarrows", "\u21C6" }, - { "leftrightharpoons", "\u21CB" }, - { "leftrightsquigarrow", "\u21AD" }, - { "LeftRightVector", "\u294E" }, - { "LeftTee", "\u22A3" }, - { "LeftTeeArrow", "\u21A4" }, - { "LeftTeeVector", "\u295A" }, - { "leftthreetimes", "\u22CB" }, - { "LeftTriangle", "\u22B2" }, - { "LeftTriangleBar", "\u29CF" }, - { "LeftTriangleEqual", "\u22B4" }, - { "LeftUpDownVector", "\u2951" }, - { "LeftUpTeeVector", "\u2960" }, - { "LeftUpVector", "\u21BF" }, - { "LeftUpVectorBar", "\u2958" }, - { "LeftVector", "\u21BC" }, - { "LeftVectorBar", "\u2952" }, - { "lEg", "\u2A8B" }, - { "leg", "\u22DA" }, - { "leq", "\u2264" }, - { "leqq", "\u2266" }, - { "leqslant", "\u2A7D" }, - { "les", "\u2A7D" }, - { "lescc", "\u2AA8" }, - { "lesdot", "\u2A7F" }, - { "lesdoto", "\u2A81" }, - { "lesdotor", "\u2A83" }, - { "lesg", "\u22DA\uFE00" }, - { "lesges", "\u2A93" }, - { "lessapprox", "\u2A85" }, - { "lessdot", "\u22D6" }, - { "lesseqgtr", "\u22DA" }, - { "lesseqqgtr", "\u2A8B" }, - { "LessEqualGreater", "\u22DA" }, - { "LessFullEqual", "\u2266" }, - { "LessGreater", "\u2276" }, - { "lessgtr", "\u2276" }, - { "LessLess", "\u2AA1" }, - { "lesssim", "\u2272" }, - { "LessSlantEqual", "\u2A7D" }, - { "LessTilde", "\u2272" }, - { "lfisht", "\u297C" }, - { "lfloor", "\u230A" }, - { "Lfr", "\uD835\uDD0F" }, - { "lfr", "\uD835\uDD29" }, - { "lg", "\u2276" }, - { "lgE", "\u2A91" }, - { "lHar", "\u2962" }, - { "lhard", "\u21BD" }, - { "lharu", "\u21BC" }, - { "lharul", "\u296A" }, - { "lhblk", "\u2584" }, - { "LJcy", "\u0409" }, - { "ljcy", "\u0459" }, - { "Ll", "\u22D8" }, - { "ll", "\u226A" }, - { "llarr", "\u21C7" }, - { "llcorner", "\u231E" }, - { "Lleftarrow", "\u21DA" }, - { "llhard", "\u296B" }, - { "lltri", "\u25FA" }, - { "Lmidot", "\u013F" }, - { "lmidot", "\u0140" }, - { "lmoust", "\u23B0" }, - { "lmoustache", "\u23B0" }, - { "lnap", "\u2A89" }, - { "lnapprox", "\u2A89" }, - { "lnE", "\u2268" }, - { "lne", "\u2A87" }, - { "lneq", "\u2A87" }, - { "lneqq", "\u2268" }, - { "lnsim", "\u22E6" }, - { "loang", "\u27EC" }, - { "loarr", "\u21FD" }, - { "lobrk", "\u27E6" }, - { "LongLeftArrow", "\u27F5" }, - { "Longleftarrow", "\u27F8" }, - { "longleftarrow", "\u27F5" }, - { "LongLeftRightArrow", "\u27F7" }, - { "Longleftrightarrow", "\u27FA" }, - { "longleftrightarrow", "\u27F7" }, - { "longmapsto", "\u27FC" }, - { "LongRightArrow", "\u27F6" }, - { "Longrightarrow", "\u27F9" }, - { "longrightarrow", "\u27F6" }, - { "looparrowleft", "\u21AB" }, - { "looparrowright", "\u21AC" }, - { "lopar", "\u2985" }, - { "Lopf", "\uD835\uDD43" }, - { "lopf", "\uD835\uDD5D" }, - { "loplus", "\u2A2D" }, - { "lotimes", "\u2A34" }, - { "lowast", "\u2217" }, - { "lowbar", "\u005F" }, - { "LowerLeftArrow", "\u2199" }, - { "LowerRightArrow", "\u2198" }, - { "loz", "\u25CA" }, - { "lozenge", "\u25CA" }, - { "lozf", "\u29EB" }, - { "lpar", "\u0028" }, - { "lparlt", "\u2993" }, - { "lrarr", "\u21C6" }, - { "lrcorner", "\u231F" }, - { "lrhar", "\u21CB" }, - { "lrhard", "\u296D" }, - { "lrm", "\u200E" }, - { "lrtri", "\u22BF" }, - { "lsaquo", "\u2039" }, - { "Lscr", "\u2112" }, - { "lscr", "\uD835\uDCC1" }, - { "Lsh", "\u21B0" }, - { "lsh", "\u21B0" }, - { "lsim", "\u2272" }, - { "lsime", "\u2A8D" }, - { "lsimg", "\u2A8F" }, - { "lsqb", "\u005B" }, - { "lsquo", "\u2018" }, - { "lsquor", "\u201A" }, - { "Lstrok", "\u0141" }, - { "lstrok", "\u0142" }, - { "LT", "\u003C" }, - { "Lt", "\u226A" }, - { "lt", "\u003C" }, - { "ltcc", "\u2AA6" }, - { "ltcir", "\u2A79" }, - { "ltdot", "\u22D6" }, - { "lthree", "\u22CB" }, - { "ltimes", "\u22C9" }, - { "ltlarr", "\u2976" }, - { "ltquest", "\u2A7B" }, - { "ltri", "\u25C3" }, - { "ltrie", "\u22B4" }, - { "ltrif", "\u25C2" }, - { "ltrPar", "\u2996" }, - { "lurdshar", "\u294A" }, - { "luruhar", "\u2966" }, - { "lvertneqq", "\u2268\uFE00" }, - { "lvnE", "\u2268\uFE00" }, - { "macr", "\u00AF" }, - { "male", "\u2642" }, - { "malt", "\u2720" }, - { "maltese", "\u2720" }, - { "Map", "\u2905" }, - { "map", "\u21A6" }, - { "mapsto", "\u21A6" }, - { "mapstodown", "\u21A7" }, - { "mapstoleft", "\u21A4" }, - { "mapstoup", "\u21A5" }, - { "marker", "\u25AE" }, - { "mcomma", "\u2A29" }, - { "Mcy", "\u041C" }, - { "mcy", "\u043C" }, - { "mdash", "\u2014" }, - { "mDDot", "\u223A" }, - { "measuredangle", "\u2221" }, - { "MediumSpace", "\u205F" }, - { "Mellintrf", "\u2133" }, - { "Mfr", "\uD835\uDD10" }, - { "mfr", "\uD835\uDD2A" }, - { "mho", "\u2127" }, - { "micro", "\u00B5" }, - { "mid", "\u2223" }, - { "midast", "\u002A" }, - { "midcir", "\u2AF0" }, - { "middot", "\u00B7" }, - { "minus", "\u2212" }, - { "minusb", "\u229F" }, - { "minusd", "\u2238" }, - { "minusdu", "\u2A2A" }, - { "MinusPlus", "\u2213" }, - { "mlcp", "\u2ADB" }, - { "mldr", "\u2026" }, - { "mnplus", "\u2213" }, - { "models", "\u22A7" }, - { "Mopf", "\uD835\uDD44" }, - { "mopf", "\uD835\uDD5E" }, - { "mp", "\u2213" }, - { "Mscr", "\u2133" }, - { "mscr", "\uD835\uDCC2" }, - { "mstpos", "\u223E" }, - { "Mu", "\u039C" }, - { "mu", "\u03BC" }, - { "multimap", "\u22B8" }, - { "mumap", "\u22B8" }, - { "nabla", "\u2207" }, - { "Nacute", "\u0143" }, - { "nacute", "\u0144" }, - { "nang", "\u2220\u20D2" }, - { "nap", "\u2249" }, - { "napE", "\u2A70\u0338" }, - { "napid", "\u224B\u0338" }, - { "napos", "\u0149" }, - { "napprox", "\u2249" }, - { "natur", "\u266E" }, - { "natural", "\u266E" }, - { "naturals", "\u2115" }, - { "nbsp", "\u00A0" }, - { "nbump", "\u224E\u0338" }, - { "nbumpe", "\u224F\u0338" }, - { "ncap", "\u2A43" }, - { "Ncaron", "\u0147" }, - { "ncaron", "\u0148" }, - { "Ncedil", "\u0145" }, - { "ncedil", "\u0146" }, - { "ncong", "\u2247" }, - { "ncongdot", "\u2A6D\u0338" }, - { "ncup", "\u2A42" }, - { "Ncy", "\u041D" }, - { "ncy", "\u043D" }, - { "ndash", "\u2013" }, - { "ne", "\u2260" }, - { "nearhk", "\u2924" }, - { "neArr", "\u21D7" }, - { "nearr", "\u2197" }, - { "nearrow", "\u2197" }, - { "nedot", "\u2250\u0338" }, - { "NegativeMediumSpace", "\u200B" }, - { "NegativeThickSpace", "\u200B" }, - { "NegativeThinSpace", "\u200B" }, - { "NegativeVeryThinSpace", "\u200B" }, - { "nequiv", "\u2262" }, - { "nesear", "\u2928" }, - { "nesim", "\u2242\u0338" }, - { "NestedGreaterGreater", "\u226B" }, - { "NestedLessLess", "\u226A" }, - { "NewLine", "\u000A" }, - { "nexist", "\u2204" }, - { "nexists", "\u2204" }, - { "Nfr", "\uD835\uDD11" }, - { "nfr", "\uD835\uDD2B" }, - { "ngE", "\u2267\u0338" }, - { "nge", "\u2271" }, - { "ngeq", "\u2271" }, - { "ngeqq", "\u2267\u0338" }, - { "ngeqslant", "\u2A7E\u0338" }, - { "nges", "\u2A7E\u0338" }, - { "nGg", "\u22D9\u0338" }, - { "ngsim", "\u2275" }, - { "nGt", "\u226B\u20D2" }, - { "ngt", "\u226F" }, - { "ngtr", "\u226F" }, - { "nGtv", "\u226B\u0338" }, - { "nhArr", "\u21CE" }, - { "nharr", "\u21AE" }, - { "nhpar", "\u2AF2" }, - { "ni", "\u220B" }, - { "nis", "\u22FC" }, - { "nisd", "\u22FA" }, - { "niv", "\u220B" }, - { "NJcy", "\u040A" }, - { "njcy", "\u045A" }, - { "nlArr", "\u21CD" }, - { "nlarr", "\u219A" }, - { "nldr", "\u2025" }, - { "nlE", "\u2266\u0338" }, - { "nle", "\u2270" }, - { "nLeftarrow", "\u21CD" }, - { "nleftarrow", "\u219A" }, - { "nLeftrightarrow", "\u21CE" }, - { "nleftrightarrow", "\u21AE" }, - { "nleq", "\u2270" }, - { "nleqq", "\u2266\u0338" }, - { "nleqslant", "\u2A7D\u0338" }, - { "nles", "\u2A7D\u0338" }, - { "nless", "\u226E" }, - { "nLl", "\u22D8\u0338" }, - { "nlsim", "\u2274" }, - { "nLt", "\u226A\u20D2" }, - { "nlt", "\u226E" }, - { "nltri", "\u22EA" }, - { "nltrie", "\u22EC" }, - { "nLtv", "\u226A\u0338" }, - { "nmid", "\u2224" }, - { "NoBreak", "\u2060" }, - { "NonBreakingSpace", "\u00A0" }, - { "Nopf", "\u2115" }, - { "nopf", "\uD835\uDD5F" }, - { "Not", "\u2AEC" }, - { "not", "\u00AC" }, - { "NotCongruent", "\u2262" }, - { "NotCupCap", "\u226D" }, - { "NotDoubleVerticalBar", "\u2226" }, - { "NotElement", "\u2209" }, - { "NotEqual", "\u2260" }, - { "NotEqualTilde", "\u2242\u0338" }, - { "NotExists", "\u2204" }, - { "NotGreater", "\u226F" }, - { "NotGreaterEqual", "\u2271" }, - { "NotGreaterFullEqual", "\u2267\u0338" }, - { "NotGreaterGreater", "\u226B\u0338" }, - { "NotGreaterLess", "\u2279" }, - { "NotGreaterSlantEqual", "\u2A7E\u0338" }, - { "NotGreaterTilde", "\u2275" }, - { "NotHumpDownHump", "\u224E\u0338" }, - { "NotHumpEqual", "\u224F\u0338" }, - { "notin", "\u2209" }, - { "notindot", "\u22F5\u0338" }, - { "notinE", "\u22F9\u0338" }, - { "notinva", "\u2209" }, - { "notinvb", "\u22F7" }, - { "notinvc", "\u22F6" }, - { "NotLeftTriangle", "\u22EA" }, - { "NotLeftTriangleBar", "\u29CF\u0338" }, - { "NotLeftTriangleEqual", "\u22EC" }, - { "NotLess", "\u226E" }, - { "NotLessEqual", "\u2270" }, - { "NotLessGreater", "\u2278" }, - { "NotLessLess", "\u226A\u0338" }, - { "NotLessSlantEqual", "\u2A7D\u0338" }, - { "NotLessTilde", "\u2274" }, - { "NotNestedGreaterGreater", "\u2AA2\u0338" }, - { "NotNestedLessLess", "\u2AA1\u0338" }, - { "notni", "\u220C" }, - { "notniva", "\u220C" }, - { "notnivb", "\u22FE" }, - { "notnivc", "\u22FD" }, - { "NotPrecedes", "\u2280" }, - { "NotPrecedesEqual", "\u2AAF\u0338" }, - { "NotPrecedesSlantEqual", "\u22E0" }, - { "NotReverseElement", "\u220C" }, - { "NotRightTriangle", "\u22EB" }, - { "NotRightTriangleBar", "\u29D0\u0338" }, - { "NotRightTriangleEqual", "\u22ED" }, - { "NotSquareSubset", "\u228F\u0338" }, - { "NotSquareSubsetEqual", "\u22E2" }, - { "NotSquareSuperset", "\u2290\u0338" }, - { "NotSquareSupersetEqual", "\u22E3" }, - { "NotSubset", "\u2282\u20D2" }, - { "NotSubsetEqual", "\u2288" }, - { "NotSucceeds", "\u2281" }, - { "NotSucceedsEqual", "\u2AB0\u0338" }, - { "NotSucceedsSlantEqual", "\u22E1" }, - { "NotSucceedsTilde", "\u227F\u0338" }, - { "NotSuperset", "\u2283\u20D2" }, - { "NotSupersetEqual", "\u2289" }, - { "NotTilde", "\u2241" }, - { "NotTildeEqual", "\u2244" }, - { "NotTildeFullEqual", "\u2247" }, - { "NotTildeTilde", "\u2249" }, - { "NotVerticalBar", "\u2224" }, - { "npar", "\u2226" }, - { "nparallel", "\u2226" }, - { "nparsl", "\u2AFD\u20E5" }, - { "npart", "\u2202\u0338" }, - { "npolint", "\u2A14" }, - { "npr", "\u2280" }, - { "nprcue", "\u22E0" }, - { "npre", "\u2AAF\u0338" }, - { "nprec", "\u2280" }, - { "npreceq", "\u2AAF\u0338" }, - { "nrArr", "\u21CF" }, - { "nrarr", "\u219B" }, - { "nrarrc", "\u2933\u0338" }, - { "nrarrw", "\u219D\u0338" }, - { "nRightarrow", "\u21CF" }, - { "nrightarrow", "\u219B" }, - { "nrtri", "\u22EB" }, - { "nrtrie", "\u22ED" }, - { "nsc", "\u2281" }, - { "nsccue", "\u22E1" }, - { "nsce", "\u2AB0\u0338" }, - { "Nscr", "\uD835\uDCA9" }, - { "nscr", "\uD835\uDCC3" }, - { "nshortmid", "\u2224" }, - { "nshortparallel", "\u2226" }, - { "nsim", "\u2241" }, - { "nsime", "\u2244" }, - { "nsimeq", "\u2244" }, - { "nsmid", "\u2224" }, - { "nspar", "\u2226" }, - { "nsqsube", "\u22E2" }, - { "nsqsupe", "\u22E3" }, - { "nsub", "\u2284" }, - { "nsubE", "\u2AC5\u0338" }, - { "nsube", "\u2288" }, - { "nsubset", "\u2282\u20D2" }, - { "nsubseteq", "\u2288" }, - { "nsubseteqq", "\u2AC5\u0338" }, - { "nsucc", "\u2281" }, - { "nsucceq", "\u2AB0\u0338" }, - { "nsup", "\u2285" }, - { "nsupE", "\u2AC6\u0338" }, - { "nsupe", "\u2289" }, - { "nsupset", "\u2283\u20D2" }, - { "nsupseteq", "\u2289" }, - { "nsupseteqq", "\u2AC6\u0338" }, - { "ntgl", "\u2279" }, - { "Ntilde", "\u00D1" }, - { "ntilde", "\u00F1" }, - { "ntlg", "\u2278" }, - { "ntriangleleft", "\u22EA" }, - { "ntrianglelefteq", "\u22EC" }, - { "ntriangleright", "\u22EB" }, - { "ntrianglerighteq", "\u22ED" }, - { "Nu", "\u039D" }, - { "nu", "\u03BD" }, - { "num", "\u0023" }, - { "numero", "\u2116" }, - { "numsp", "\u2007" }, - { "nvap", "\u224D\u20D2" }, - { "nVDash", "\u22AF" }, - { "nVdash", "\u22AE" }, - { "nvDash", "\u22AD" }, - { "nvdash", "\u22AC" }, - { "nvge", "\u2265\u20D2" }, - { "nvgt", "\u003E\u20D2" }, - { "nvHarr", "\u2904" }, - { "nvinfin", "\u29DE" }, - { "nvlArr", "\u2902" }, - { "nvle", "\u2264\u20D2" }, - { "nvlt", "\u003C\u20D2" }, - { "nvltrie", "\u22B4\u20D2" }, - { "nvrArr", "\u2903" }, - { "nvrtrie", "\u22B5\u20D2" }, - { "nvsim", "\u223C\u20D2" }, - { "nwarhk", "\u2923" }, - { "nwArr", "\u21D6" }, - { "nwarr", "\u2196" }, - { "nwarrow", "\u2196" }, - { "nwnear", "\u2927" }, - { "Oacute", "\u00D3" }, - { "oacute", "\u00F3" }, - { "oast", "\u229B" }, - { "ocir", "\u229A" }, - { "Ocirc", "\u00D4" }, - { "ocirc", "\u00F4" }, - { "Ocy", "\u041E" }, - { "ocy", "\u043E" }, - { "odash", "\u229D" }, - { "Odblac", "\u0150" }, - { "odblac", "\u0151" }, - { "odiv", "\u2A38" }, - { "odot", "\u2299" }, - { "odsold", "\u29BC" }, - { "OElig", "\u0152" }, - { "oelig", "\u0153" }, - { "ofcir", "\u29BF" }, - { "Ofr", "\uD835\uDD12" }, - { "ofr", "\uD835\uDD2C" }, - { "ogon", "\u02DB" }, - { "Ograve", "\u00D2" }, - { "ograve", "\u00F2" }, - { "ogt", "\u29C1" }, - { "ohbar", "\u29B5" }, - { "ohm", "\u03A9" }, - { "oint", "\u222E" }, - { "olarr", "\u21BA" }, - { "olcir", "\u29BE" }, - { "olcross", "\u29BB" }, - { "oline", "\u203E" }, - { "olt", "\u29C0" }, - { "Omacr", "\u014C" }, - { "omacr", "\u014D" }, - { "Omega", "\u03A9" }, - { "omega", "\u03C9" }, - { "Omicron", "\u039F" }, - { "omicron", "\u03BF" }, - { "omid", "\u29B6" }, - { "ominus", "\u2296" }, - { "Oopf", "\uD835\uDD46" }, - { "oopf", "\uD835\uDD60" }, - { "opar", "\u29B7" }, - { "OpenCurlyDoubleQuote", "\u201C" }, - { "OpenCurlyQuote", "\u2018" }, - { "operp", "\u29B9" }, - { "oplus", "\u2295" }, - { "Or", "\u2A54" }, - { "or", "\u2228" }, - { "orarr", "\u21BB" }, - { "ord", "\u2A5D" }, - { "order", "\u2134" }, - { "orderof", "\u2134" }, - { "ordf", "\u00AA" }, - { "ordm", "\u00BA" }, - { "origof", "\u22B6" }, - { "oror", "\u2A56" }, - { "orslope", "\u2A57" }, - { "orv", "\u2A5B" }, - { "oS", "\u24C8" }, - { "Oscr", "\uD835\uDCAA" }, - { "oscr", "\u2134" }, - { "Oslash", "\u00D8" }, - { "oslash", "\u00F8" }, - { "osol", "\u2298" }, - { "Otilde", "\u00D5" }, - { "otilde", "\u00F5" }, - { "Otimes", "\u2A37" }, - { "otimes", "\u2297" }, - { "otimesas", "\u2A36" }, - { "Ouml", "\u00D6" }, - { "ouml", "\u00F6" }, - { "ovbar", "\u233D" }, - { "OverBar", "\u203E" }, - { "OverBrace", "\u23DE" }, - { "OverBracket", "\u23B4" }, - { "OverParenthesis", "\u23DC" }, - { "par", "\u2225" }, - { "para", "\u00B6" }, - { "parallel", "\u2225" }, - { "parsim", "\u2AF3" }, - { "parsl", "\u2AFD" }, - { "part", "\u2202" }, - { "PartialD", "\u2202" }, - { "Pcy", "\u041F" }, - { "pcy", "\u043F" }, - { "percnt", "\u0025" }, - { "period", "\u002E" }, - { "permil", "\u2030" }, - { "perp", "\u22A5" }, - { "pertenk", "\u2031" }, - { "Pfr", "\uD835\uDD13" }, - { "pfr", "\uD835\uDD2D" }, - { "Phi", "\u03A6" }, - { "phi", "\u03C6" }, - { "phiv", "\u03D5" }, - { "phmmat", "\u2133" }, - { "phone", "\u260E" }, - { "Pi", "\u03A0" }, - { "pi", "\u03C0" }, - { "pitchfork", "\u22D4" }, - { "piv", "\u03D6" }, - { "planck", "\u210F" }, - { "planckh", "\u210E" }, - { "plankv", "\u210F" }, - { "plus", "\u002B" }, - { "plusacir", "\u2A23" }, - { "plusb", "\u229E" }, - { "pluscir", "\u2A22" }, - { "plusdo", "\u2214" }, - { "plusdu", "\u2A25" }, - { "pluse", "\u2A72" }, - { "PlusMinus", "\u00B1" }, - { "plusmn", "\u00B1" }, - { "plussim", "\u2A26" }, - { "plustwo", "\u2A27" }, - { "pm", "\u00B1" }, - { "Poincareplane", "\u210C" }, - { "pointint", "\u2A15" }, - { "Popf", "\u2119" }, - { "popf", "\uD835\uDD61" }, - { "pound", "\u00A3" }, - { "Pr", "\u2ABB" }, - { "pr", "\u227A" }, - { "prap", "\u2AB7" }, - { "prcue", "\u227C" }, - { "prE", "\u2AB3" }, - { "pre", "\u2AAF" }, - { "prec", "\u227A" }, - { "precapprox", "\u2AB7" }, - { "preccurlyeq", "\u227C" }, - { "Precedes", "\u227A" }, - { "PrecedesEqual", "\u2AAF" }, - { "PrecedesSlantEqual", "\u227C" }, - { "PrecedesTilde", "\u227E" }, - { "preceq", "\u2AAF" }, - { "precnapprox", "\u2AB9" }, - { "precneqq", "\u2AB5" }, - { "precnsim", "\u22E8" }, - { "precsim", "\u227E" }, - { "Prime", "\u2033" }, - { "prime", "\u2032" }, - { "primes", "\u2119" }, - { "prnap", "\u2AB9" }, - { "prnE", "\u2AB5" }, - { "prnsim", "\u22E8" }, - { "prod", "\u220F" }, - { "Product", "\u220F" }, - { "profalar", "\u232E" }, - { "profline", "\u2312" }, - { "profsurf", "\u2313" }, - { "prop", "\u221D" }, - { "Proportion", "\u2237" }, - { "Proportional", "\u221D" }, - { "propto", "\u221D" }, - { "prsim", "\u227E" }, - { "prurel", "\u22B0" }, - { "Pscr", "\uD835\uDCAB" }, - { "pscr", "\uD835\uDCC5" }, - { "Psi", "\u03A8" }, - { "psi", "\u03C8" }, - { "puncsp", "\u2008" }, - { "Qfr", "\uD835\uDD14" }, - { "qfr", "\uD835\uDD2E" }, - { "qint", "\u2A0C" }, - { "Qopf", "\u211A" }, - { "qopf", "\uD835\uDD62" }, - { "qprime", "\u2057" }, - { "Qscr", "\uD835\uDCAC" }, - { "qscr", "\uD835\uDCC6" }, - { "quaternions", "\u210D" }, - { "quatint", "\u2A16" }, - { "quest", "\u003F" }, - { "questeq", "\u225F" }, - { "QUOT", "\u0022" }, - { "quot", "\u0022" }, - { "rAarr", "\u21DB" }, - { "race", "\u223D\u0331" }, - { "Racute", "\u0154" }, - { "racute", "\u0155" }, - { "radic", "\u221A" }, - { "raemptyv", "\u29B3" }, - { "Rang", "\u27EB" }, - { "rang", "\u27E9" }, - { "rangd", "\u2992" }, - { "range", "\u29A5" }, - { "rangle", "\u27E9" }, - { "raquo", "\u00BB" }, - { "Rarr", "\u21A0" }, - { "rArr", "\u21D2" }, - { "rarr", "\u2192" }, - { "rarrap", "\u2975" }, - { "rarrb", "\u21E5" }, - { "rarrbfs", "\u2920" }, - { "rarrc", "\u2933" }, - { "rarrfs", "\u291E" }, - { "rarrhk", "\u21AA" }, - { "rarrlp", "\u21AC" }, - { "rarrpl", "\u2945" }, - { "rarrsim", "\u2974" }, - { "Rarrtl", "\u2916" }, - { "rarrtl", "\u21A3" }, - { "rarrw", "\u219D" }, - { "rAtail", "\u291C" }, - { "ratail", "\u291A" }, - { "ratio", "\u2236" }, - { "rationals", "\u211A" }, - { "RBarr", "\u2910" }, - { "rBarr", "\u290F" }, - { "rbarr", "\u290D" }, - { "rbbrk", "\u2773" }, - { "rbrace", "\u007D" }, - { "rbrack", "\u005D" }, - { "rbrke", "\u298C" }, - { "rbrksld", "\u298E" }, - { "rbrkslu", "\u2990" }, - { "Rcaron", "\u0158" }, - { "rcaron", "\u0159" }, - { "Rcedil", "\u0156" }, - { "rcedil", "\u0157" }, - { "rceil", "\u2309" }, - { "rcub", "\u007D" }, - { "Rcy", "\u0420" }, - { "rcy", "\u0440" }, - { "rdca", "\u2937" }, - { "rdldhar", "\u2969" }, - { "rdquo", "\u201D" }, - { "rdquor", "\u201D" }, - { "rdsh", "\u21B3" }, - { "Re", "\u211C" }, - { "real", "\u211C" }, - { "realine", "\u211B" }, - { "realpart", "\u211C" }, - { "reals", "\u211D" }, - { "rect", "\u25AD" }, - { "REG", "\u00AE" }, - { "reg", "\u00AE" }, - { "ReverseElement", "\u220B" }, - { "ReverseEquilibrium", "\u21CB" }, - { "ReverseUpEquilibrium", "\u296F" }, - { "rfisht", "\u297D" }, - { "rfloor", "\u230B" }, - { "Rfr", "\u211C" }, - { "rfr", "\uD835\uDD2F" }, - { "rHar", "\u2964" }, - { "rhard", "\u21C1" }, - { "rharu", "\u21C0" }, - { "rharul", "\u296C" }, - { "Rho", "\u03A1" }, - { "rho", "\u03C1" }, - { "rhov", "\u03F1" }, - { "RightAngleBracket", "\u27E9" }, - { "RightArrow", "\u2192" }, - { "Rightarrow", "\u21D2" }, - { "rightarrow", "\u2192" }, - { "RightArrowBar", "\u21E5" }, - { "RightArrowLeftArrow", "\u21C4" }, - { "rightarrowtail", "\u21A3" }, - { "RightCeiling", "\u2309" }, - { "RightDoubleBracket", "\u27E7" }, - { "RightDownTeeVector", "\u295D" }, - { "RightDownVector", "\u21C2" }, - { "RightDownVectorBar", "\u2955" }, - { "RightFloor", "\u230B" }, - { "rightharpoondown", "\u21C1" }, - { "rightharpoonup", "\u21C0" }, - { "rightleftarrows", "\u21C4" }, - { "rightleftharpoons", "\u21CC" }, - { "rightrightarrows", "\u21C9" }, - { "rightsquigarrow", "\u219D" }, - { "RightTee", "\u22A2" }, - { "RightTeeArrow", "\u21A6" }, - { "RightTeeVector", "\u295B" }, - { "rightthreetimes", "\u22CC" }, - { "RightTriangle", "\u22B3" }, - { "RightTriangleBar", "\u29D0" }, - { "RightTriangleEqual", "\u22B5" }, - { "RightUpDownVector", "\u294F" }, - { "RightUpTeeVector", "\u295C" }, - { "RightUpVector", "\u21BE" }, - { "RightUpVectorBar", "\u2954" }, - { "RightVector", "\u21C0" }, - { "RightVectorBar", "\u2953" }, - { "ring", "\u02DA" }, - { "risingdotseq", "\u2253" }, - { "rlarr", "\u21C4" }, - { "rlhar", "\u21CC" }, - { "rlm", "\u200F" }, - { "rmoust", "\u23B1" }, - { "rmoustache", "\u23B1" }, - { "rnmid", "\u2AEE" }, - { "roang", "\u27ED" }, - { "roarr", "\u21FE" }, - { "robrk", "\u27E7" }, - { "ropar", "\u2986" }, - { "Ropf", "\u211D" }, - { "ropf", "\uD835\uDD63" }, - { "roplus", "\u2A2E" }, - { "rotimes", "\u2A35" }, - { "RoundImplies", "\u2970" }, - { "rpar", "\u0029" }, - { "rpargt", "\u2994" }, - { "rppolint", "\u2A12" }, - { "rrarr", "\u21C9" }, - { "Rrightarrow", "\u21DB" }, - { "rsaquo", "\u203A" }, - { "Rscr", "\u211B" }, - { "rscr", "\uD835\uDCC7" }, - { "Rsh", "\u21B1" }, - { "rsh", "\u21B1" }, - { "rsqb", "\u005D" }, - { "rsquo", "\u2019" }, - { "rsquor", "\u2019" }, - { "rthree", "\u22CC" }, - { "rtimes", "\u22CA" }, - { "rtri", "\u25B9" }, - { "rtrie", "\u22B5" }, - { "rtrif", "\u25B8" }, - { "rtriltri", "\u29CE" }, - { "RuleDelayed", "\u29F4" }, - { "ruluhar", "\u2968" }, - { "rx", "\u211E" }, - { "Sacute", "\u015A" }, - { "sacute", "\u015B" }, - { "sbquo", "\u201A" }, - { "Sc", "\u2ABC" }, - { "sc", "\u227B" }, - { "scap", "\u2AB8" }, - { "Scaron", "\u0160" }, - { "scaron", "\u0161" }, - { "sccue", "\u227D" }, - { "scE", "\u2AB4" }, - { "sce", "\u2AB0" }, - { "Scedil", "\u015E" }, - { "scedil", "\u015F" }, - { "Scirc", "\u015C" }, - { "scirc", "\u015D" }, - { "scnap", "\u2ABA" }, - { "scnE", "\u2AB6" }, - { "scnsim", "\u22E9" }, - { "scpolint", "\u2A13" }, - { "scsim", "\u227F" }, - { "Scy", "\u0421" }, - { "scy", "\u0441" }, - { "sdot", "\u22C5" }, - { "sdotb", "\u22A1" }, - { "sdote", "\u2A66" }, - { "searhk", "\u2925" }, - { "seArr", "\u21D8" }, - { "searr", "\u2198" }, - { "searrow", "\u2198" }, - { "sect", "\u00A7" }, - { "semi", "\u003B" }, - { "seswar", "\u2929" }, - { "setminus", "\u2216" }, - { "setmn", "\u2216" }, - { "sext", "\u2736" }, - { "Sfr", "\uD835\uDD16" }, - { "sfr", "\uD835\uDD30" }, - { "sfrown", "\u2322" }, - { "sharp", "\u266F" }, - { "SHCHcy", "\u0429" }, - { "shchcy", "\u0449" }, - { "SHcy", "\u0428" }, - { "shcy", "\u0448" }, - { "ShortDownArrow", "\u2193" }, - { "ShortLeftArrow", "\u2190" }, - { "shortmid", "\u2223" }, - { "shortparallel", "\u2225" }, - { "ShortRightArrow", "\u2192" }, - { "ShortUpArrow", "\u2191" }, - { "shy", "\u00AD" }, - { "Sigma", "\u03A3" }, - { "sigma", "\u03C3" }, - { "sigmaf", "\u03C2" }, - { "sigmav", "\u03C2" }, - { "sim", "\u223C" }, - { "simdot", "\u2A6A" }, - { "sime", "\u2243" }, - { "simeq", "\u2243" }, - { "simg", "\u2A9E" }, - { "simgE", "\u2AA0" }, - { "siml", "\u2A9D" }, - { "simlE", "\u2A9F" }, - { "simne", "\u2246" }, - { "simplus", "\u2A24" }, - { "simrarr", "\u2972" }, - { "slarr", "\u2190" }, - { "SmallCircle", "\u2218" }, - { "smallsetminus", "\u2216" }, - { "smashp", "\u2A33" }, - { "smeparsl", "\u29E4" }, - { "smid", "\u2223" }, - { "smile", "\u2323" }, - { "smt", "\u2AAA" }, - { "smte", "\u2AAC" }, - { "smtes", "\u2AAC\uFE00" }, - { "SOFTcy", "\u042C" }, - { "softcy", "\u044C" }, - { "sol", "\u002F" }, - { "solb", "\u29C4" }, - { "solbar", "\u233F" }, - { "Sopf", "\uD835\uDD4A" }, - { "sopf", "\uD835\uDD64" }, - { "spades", "\u2660" }, - { "spadesuit", "\u2660" }, - { "spar", "\u2225" }, - { "sqcap", "\u2293" }, - { "sqcaps", "\u2293\uFE00" }, - { "sqcup", "\u2294" }, - { "sqcups", "\u2294\uFE00" }, - { "Sqrt", "\u221A" }, - { "sqsub", "\u228F" }, - { "sqsube", "\u2291" }, - { "sqsubset", "\u228F" }, - { "sqsubseteq", "\u2291" }, - { "sqsup", "\u2290" }, - { "sqsupe", "\u2292" }, - { "sqsupset", "\u2290" }, - { "sqsupseteq", "\u2292" }, - { "squ", "\u25A1" }, - { "Square", "\u25A1" }, - { "square", "\u25A1" }, - { "SquareIntersection", "\u2293" }, - { "SquareSubset", "\u228F" }, - { "SquareSubsetEqual", "\u2291" }, - { "SquareSuperset", "\u2290" }, - { "SquareSupersetEqual", "\u2292" }, - { "SquareUnion", "\u2294" }, - { "squarf", "\u25AA" }, - { "squf", "\u25AA" }, - { "srarr", "\u2192" }, - { "Sscr", "\uD835\uDCAE" }, - { "sscr", "\uD835\uDCC8" }, - { "ssetmn", "\u2216" }, - { "ssmile", "\u2323" }, - { "sstarf", "\u22C6" }, - { "Star", "\u22C6" }, - { "star", "\u2606" }, - { "starf", "\u2605" }, - { "straightepsilon", "\u03F5" }, - { "straightphi", "\u03D5" }, - { "strns", "\u00AF" }, - { "Sub", "\u22D0" }, - { "sub", "\u2282" }, - { "subdot", "\u2ABD" }, - { "subE", "\u2AC5" }, - { "sube", "\u2286" }, - { "subedot", "\u2AC3" }, - { "submult", "\u2AC1" }, - { "subnE", "\u2ACB" }, - { "subne", "\u228A" }, - { "subplus", "\u2ABF" }, - { "subrarr", "\u2979" }, - { "Subset", "\u22D0" }, - { "subset", "\u2282" }, - { "subseteq", "\u2286" }, - { "subseteqq", "\u2AC5" }, - { "SubsetEqual", "\u2286" }, - { "subsetneq", "\u228A" }, - { "subsetneqq", "\u2ACB" }, - { "subsim", "\u2AC7" }, - { "subsub", "\u2AD5" }, - { "subsup", "\u2AD3" }, - { "succ", "\u227B" }, - { "succapprox", "\u2AB8" }, - { "succcurlyeq", "\u227D" }, - { "Succeeds", "\u227B" }, - { "SucceedsEqual", "\u2AB0" }, - { "SucceedsSlantEqual", "\u227D" }, - { "SucceedsTilde", "\u227F" }, - { "succeq", "\u2AB0" }, - { "succnapprox", "\u2ABA" }, - { "succneqq", "\u2AB6" }, - { "succnsim", "\u22E9" }, - { "succsim", "\u227F" }, - { "SuchThat", "\u220B" }, - { "Sum", "\u2211" }, - { "sum", "\u2211" }, - { "sung", "\u266A" }, - { "Sup", "\u22D1" }, - { "sup", "\u2283" }, - { "sup1", "\u00B9" }, - { "sup2", "\u00B2" }, - { "sup3", "\u00B3" }, - { "supdot", "\u2ABE" }, - { "supdsub", "\u2AD8" }, - { "supE", "\u2AC6" }, - { "supe", "\u2287" }, - { "supedot", "\u2AC4" }, - { "Superset", "\u2283" }, - { "SupersetEqual", "\u2287" }, - { "suphsol", "\u27C9" }, - { "suphsub", "\u2AD7" }, - { "suplarr", "\u297B" }, - { "supmult", "\u2AC2" }, - { "supnE", "\u2ACC" }, - { "supne", "\u228B" }, - { "supplus", "\u2AC0" }, - { "Supset", "\u22D1" }, - { "supset", "\u2283" }, - { "supseteq", "\u2287" }, - { "supseteqq", "\u2AC6" }, - { "supsetneq", "\u228B" }, - { "supsetneqq", "\u2ACC" }, - { "supsim", "\u2AC8" }, - { "supsub", "\u2AD4" }, - { "supsup", "\u2AD6" }, - { "swarhk", "\u2926" }, - { "swArr", "\u21D9" }, - { "swarr", "\u2199" }, - { "swarrow", "\u2199" }, - { "swnwar", "\u292A" }, - { "szlig", "\u00DF" }, - { "Tab", "\u0009" }, - { "target", "\u2316" }, - { "Tau", "\u03A4" }, - { "tau", "\u03C4" }, - { "tbrk", "\u23B4" }, - { "Tcaron", "\u0164" }, - { "tcaron", "\u0165" }, - { "Tcedil", "\u0162" }, - { "tcedil", "\u0163" }, - { "Tcy", "\u0422" }, - { "tcy", "\u0442" }, - { "tdot", "\u20DB" }, - { "telrec", "\u2315" }, - { "Tfr", "\uD835\uDD17" }, - { "tfr", "\uD835\uDD31" }, - { "there4", "\u2234" }, - { "Therefore", "\u2234" }, - { "therefore", "\u2234" }, - { "Theta", "\u0398" }, - { "theta", "\u03B8" }, - { "thetasym", "\u03D1" }, - { "thetav", "\u03D1" }, - { "thickapprox", "\u2248" }, - { "thicksim", "\u223C" }, - { "ThickSpace", "\u205F\u200A" }, - { "thinsp", "\u2009" }, - { "ThinSpace", "\u2009" }, - { "thkap", "\u2248" }, - { "thksim", "\u223C" }, - { "THORN", "\u00DE" }, - { "thorn", "\u00FE" }, - { "Tilde", "\u223C" }, - { "tilde", "\u02DC" }, - { "TildeEqual", "\u2243" }, - { "TildeFullEqual", "\u2245" }, - { "TildeTilde", "\u2248" }, - { "times", "\u00D7" }, - { "timesb", "\u22A0" }, - { "timesbar", "\u2A31" }, - { "timesd", "\u2A30" }, - { "tint", "\u222D" }, - { "toea", "\u2928" }, - { "top", "\u22A4" }, - { "topbot", "\u2336" }, - { "topcir", "\u2AF1" }, - { "Topf", "\uD835\uDD4B" }, - { "topf", "\uD835\uDD65" }, - { "topfork", "\u2ADA" }, - { "tosa", "\u2929" }, - { "tprime", "\u2034" }, - { "TRADE", "\u2122" }, - { "trade", "\u2122" }, - { "triangle", "\u25B5" }, - { "triangledown", "\u25BF" }, - { "triangleleft", "\u25C3" }, - { "trianglelefteq", "\u22B4" }, - { "triangleq", "\u225C" }, - { "triangleright", "\u25B9" }, - { "trianglerighteq", "\u22B5" }, - { "tridot", "\u25EC" }, - { "trie", "\u225C" }, - { "triminus", "\u2A3A" }, - { "TripleDot", "\u20DB" }, - { "triplus", "\u2A39" }, - { "trisb", "\u29CD" }, - { "tritime", "\u2A3B" }, - { "trpezium", "\u23E2" }, - { "Tscr", "\uD835\uDCAF" }, - { "tscr", "\uD835\uDCC9" }, - { "TScy", "\u0426" }, - { "tscy", "\u0446" }, - { "TSHcy", "\u040B" }, - { "tshcy", "\u045B" }, - { "Tstrok", "\u0166" }, - { "tstrok", "\u0167" }, - { "twixt", "\u226C" }, - { "twoheadleftarrow", "\u219E" }, - { "twoheadrightarrow", "\u21A0" }, - { "Uacute", "\u00DA" }, - { "uacute", "\u00FA" }, - { "Uarr", "\u219F" }, - { "uArr", "\u21D1" }, - { "uarr", "\u2191" }, - { "Uarrocir", "\u2949" }, - { "Ubrcy", "\u040E" }, - { "ubrcy", "\u045E" }, - { "Ubreve", "\u016C" }, - { "ubreve", "\u016D" }, - { "Ucirc", "\u00DB" }, - { "ucirc", "\u00FB" }, - { "Ucy", "\u0423" }, - { "ucy", "\u0443" }, - { "udarr", "\u21C5" }, - { "Udblac", "\u0170" }, - { "udblac", "\u0171" }, - { "udhar", "\u296E" }, - { "ufisht", "\u297E" }, - { "Ufr", "\uD835\uDD18" }, - { "ufr", "\uD835\uDD32" }, - { "Ugrave", "\u00D9" }, - { "ugrave", "\u00F9" }, - { "uHar", "\u2963" }, - { "uharl", "\u21BF" }, - { "uharr", "\u21BE" }, - { "uhblk", "\u2580" }, - { "ulcorn", "\u231C" }, - { "ulcorner", "\u231C" }, - { "ulcrop", "\u230F" }, - { "ultri", "\u25F8" }, - { "Umacr", "\u016A" }, - { "umacr", "\u016B" }, - { "uml", "\u00A8" }, - { "UnderBar", "\u005F" }, - { "UnderBrace", "\u23DF" }, - { "UnderBracket", "\u23B5" }, - { "UnderParenthesis", "\u23DD" }, - { "Union", "\u22C3" }, - { "UnionPlus", "\u228E" }, - { "Uogon", "\u0172" }, - { "uogon", "\u0173" }, - { "Uopf", "\uD835\uDD4C" }, - { "uopf", "\uD835\uDD66" }, - { "UpArrow", "\u2191" }, - { "Uparrow", "\u21D1" }, - { "uparrow", "\u2191" }, - { "UpArrowBar", "\u2912" }, - { "UpArrowDownArrow", "\u21C5" }, - { "UpDownArrow", "\u2195" }, - { "Updownarrow", "\u21D5" }, - { "updownarrow", "\u2195" }, - { "UpEquilibrium", "\u296E" }, - { "upharpoonleft", "\u21BF" }, - { "upharpoonright", "\u21BE" }, - { "uplus", "\u228E" }, - { "UpperLeftArrow", "\u2196" }, - { "UpperRightArrow", "\u2197" }, - { "Upsi", "\u03D2" }, - { "upsi", "\u03C5" }, - { "upsih", "\u03D2" }, - { "Upsilon", "\u03A5" }, - { "upsilon", "\u03C5" }, - { "UpTee", "\u22A5" }, - { "UpTeeArrow", "\u21A5" }, - { "upuparrows", "\u21C8" }, - { "urcorn", "\u231D" }, - { "urcorner", "\u231D" }, - { "urcrop", "\u230E" }, - { "Uring", "\u016E" }, - { "uring", "\u016F" }, - { "urtri", "\u25F9" }, - { "Uscr", "\uD835\uDCB0" }, - { "uscr", "\uD835\uDCCA" }, - { "utdot", "\u22F0" }, - { "Utilde", "\u0168" }, - { "utilde", "\u0169" }, - { "utri", "\u25B5" }, - { "utrif", "\u25B4" }, - { "uuarr", "\u21C8" }, - { "Uuml", "\u00DC" }, - { "uuml", "\u00FC" }, - { "uwangle", "\u29A7" }, - { "vangrt", "\u299C" }, - { "varepsilon", "\u03F5" }, - { "varkappa", "\u03F0" }, - { "varnothing", "\u2205" }, - { "varphi", "\u03D5" }, - { "varpi", "\u03D6" }, - { "varpropto", "\u221D" }, - { "vArr", "\u21D5" }, - { "varr", "\u2195" }, - { "varrho", "\u03F1" }, - { "varsigma", "\u03C2" }, - { "varsubsetneq", "\u228A\uFE00" }, - { "varsubsetneqq", "\u2ACB\uFE00" }, - { "varsupsetneq", "\u228B\uFE00" }, - { "varsupsetneqq", "\u2ACC\uFE00" }, - { "vartheta", "\u03D1" }, - { "vartriangleleft", "\u22B2" }, - { "vartriangleright", "\u22B3" }, - { "Vbar", "\u2AEB" }, - { "vBar", "\u2AE8" }, - { "vBarv", "\u2AE9" }, - { "Vcy", "\u0412" }, - { "vcy", "\u0432" }, - { "VDash", "\u22AB" }, - { "Vdash", "\u22A9" }, - { "vDash", "\u22A8" }, - { "vdash", "\u22A2" }, - { "Vdashl", "\u2AE6" }, - { "Vee", "\u22C1" }, - { "vee", "\u2228" }, - { "veebar", "\u22BB" }, - { "veeeq", "\u225A" }, - { "vellip", "\u22EE" }, - { "Verbar", "\u2016" }, - { "verbar", "\u007C" }, - { "Vert", "\u2016" }, - { "vert", "\u007C" }, - { "VerticalBar", "\u2223" }, - { "VerticalLine", "\u007C" }, - { "VerticalSeparator", "\u2758" }, - { "VerticalTilde", "\u2240" }, - { "VeryThinSpace", "\u200A" }, - { "Vfr", "\uD835\uDD19" }, - { "vfr", "\uD835\uDD33" }, - { "vltri", "\u22B2" }, - { "vnsub", "\u2282\u20D2" }, - { "vnsup", "\u2283\u20D2" }, - { "Vopf", "\uD835\uDD4D" }, - { "vopf", "\uD835\uDD67" }, - { "vprop", "\u221D" }, - { "vrtri", "\u22B3" }, - { "Vscr", "\uD835\uDCB1" }, - { "vscr", "\uD835\uDCCB" }, - { "vsubnE", "\u2ACB\uFE00" }, - { "vsubne", "\u228A\uFE00" }, - { "vsupnE", "\u2ACC\uFE00" }, - { "vsupne", "\u228B\uFE00" }, - { "Vvdash", "\u22AA" }, - { "vzigzag", "\u299A" }, - { "Wcirc", "\u0174" }, - { "wcirc", "\u0175" }, - { "wedbar", "\u2A5F" }, - { "Wedge", "\u22C0" }, - { "wedge", "\u2227" }, - { "wedgeq", "\u2259" }, - { "weierp", "\u2118" }, - { "Wfr", "\uD835\uDD1A" }, - { "wfr", "\uD835\uDD34" }, - { "Wopf", "\uD835\uDD4E" }, - { "wopf", "\uD835\uDD68" }, - { "wp", "\u2118" }, - { "wr", "\u2240" }, - { "wreath", "\u2240" }, - { "Wscr", "\uD835\uDCB2" }, - { "wscr", "\uD835\uDCCC" }, - { "xcap", "\u22C2" }, - { "xcirc", "\u25EF" }, - { "xcup", "\u22C3" }, - { "xdtri", "\u25BD" }, - { "Xfr", "\uD835\uDD1B" }, - { "xfr", "\uD835\uDD35" }, - { "xhArr", "\u27FA" }, - { "xharr", "\u27F7" }, - { "Xi", "\u039E" }, - { "xi", "\u03BE" }, - { "xlArr", "\u27F8" }, - { "xlarr", "\u27F5" }, - { "xmap", "\u27FC" }, - { "xnis", "\u22FB" }, - { "xodot", "\u2A00" }, - { "Xopf", "\uD835\uDD4F" }, - { "xopf", "\uD835\uDD69" }, - { "xoplus", "\u2A01" }, - { "xotime", "\u2A02" }, - { "xrArr", "\u27F9" }, - { "xrarr", "\u27F6" }, - { "Xscr", "\uD835\uDCB3" }, - { "xscr", "\uD835\uDCCD" }, - { "xsqcup", "\u2A06" }, - { "xuplus", "\u2A04" }, - { "xutri", "\u25B3" }, - { "xvee", "\u22C1" }, - { "xwedge", "\u22C0" }, - { "Yacute", "\u00DD" }, - { "yacute", "\u00FD" }, - { "YAcy", "\u042F" }, - { "yacy", "\u044F" }, - { "Ycirc", "\u0176" }, - { "ycirc", "\u0177" }, - { "Ycy", "\u042B" }, - { "ycy", "\u044B" }, - { "yen", "\u00A5" }, - { "Yfr", "\uD835\uDD1C" }, - { "yfr", "\uD835\uDD36" }, - { "YIcy", "\u0407" }, - { "yicy", "\u0457" }, - { "Yopf", "\uD835\uDD50" }, - { "yopf", "\uD835\uDD6A" }, - { "Yscr", "\uD835\uDCB4" }, - { "yscr", "\uD835\uDCCE" }, - { "YUcy", "\u042E" }, - { "yucy", "\u044E" }, - { "Yuml", "\u0178" }, - { "yuml", "\u00FF" }, - { "Zacute", "\u0179" }, - { "zacute", "\u017A" }, - { "Zcaron", "\u017D" }, - { "zcaron", "\u017E" }, - { "Zcy", "\u0417" }, - { "zcy", "\u0437" }, - { "Zdot", "\u017B" }, - { "zdot", "\u017C" }, - { "zeetrf", "\u2128" }, - { "ZeroWidthSpace", "\u200B" }, - { "Zeta", "\u0396" }, - { "zeta", "\u03B6" }, - { "Zfr", "\u2128" }, - { "zfr", "\uD835\uDD37" }, - { "ZHcy", "\u0416" }, - { "zhcy", "\u0436" }, - { "zigrarr", "\u21DD" }, - { "Zopf", "\u2124" }, - { "zopf", "\uD835\uDD6B" }, - { "Zscr", "\uD835\uDCB5" }, - { "zscr", "\uD835\uDCCF" }, - { "zwj", "\u200D" }, - { "zwnj", "\u200C" } - }; -#endregion + sb.Append((char)utf32); + } + else + { + utf32 -= 65536; + sb.Append((char)((uint)utf32 / 1024 + 55296)); + sb.Append((char)((uint)utf32 % 1024 + 56320)); + } } + + #region [ EntityMap ] + /// + /// Source: http://www.w3.org/html/wg/drafts/html/master/syntax.html#named-character-references + /// + private static readonly CompactPrefixTree EntityMap = new CompactPrefixTree(2125, 3385, 3510) + { + { "Aacute", "\u00C1" }, + { "aacute", "\u00E1" }, + { "Abreve", "\u0102" }, + { "abreve", "\u0103" }, + { "ac", "\u223E" }, + { "acd", "\u223F" }, + { "acE", "\u223E\u0333" }, + { "Acirc", "\u00C2" }, + { "acirc", "\u00E2" }, + { "acute", "\u00B4" }, + { "Acy", "\u0410" }, + { "acy", "\u0430" }, + { "AElig", "\u00C6" }, + { "aelig", "\u00E6" }, + { "af", "\u2061" }, + { "Afr", "\uD835\uDD04" }, + { "afr", "\uD835\uDD1E" }, + { "Agrave", "\u00C0" }, + { "agrave", "\u00E0" }, + { "alefsym", "\u2135" }, + { "aleph", "\u2135" }, + { "Alpha", "\u0391" }, + { "alpha", "\u03B1" }, + { "Amacr", "\u0100" }, + { "amacr", "\u0101" }, + { "amalg", "\u2A3F" }, + { "AMP", "\u0026" }, + { "amp", "\u0026" }, + { "And", "\u2A53" }, + { "and", "\u2227" }, + { "andand", "\u2A55" }, + { "andd", "\u2A5C" }, + { "andslope", "\u2A58" }, + { "andv", "\u2A5A" }, + { "ang", "\u2220" }, + { "ange", "\u29A4" }, + { "angle", "\u2220" }, + { "angmsd", "\u2221" }, + { "angmsdaa", "\u29A8" }, + { "angmsdab", "\u29A9" }, + { "angmsdac", "\u29AA" }, + { "angmsdad", "\u29AB" }, + { "angmsdae", "\u29AC" }, + { "angmsdaf", "\u29AD" }, + { "angmsdag", "\u29AE" }, + { "angmsdah", "\u29AF" }, + { "angrt", "\u221F" }, + { "angrtvb", "\u22BE" }, + { "angrtvbd", "\u299D" }, + { "angsph", "\u2222" }, + { "angst", "\u00C5" }, + { "angzarr", "\u237C" }, + { "Aogon", "\u0104" }, + { "aogon", "\u0105" }, + { "Aopf", "\uD835\uDD38" }, + { "aopf", "\uD835\uDD52" }, + { "ap", "\u2248" }, + { "apacir", "\u2A6F" }, + { "apE", "\u2A70" }, + { "ape", "\u224A" }, + { "apid", "\u224B" }, + { "apos", "\u0027" }, + { "ApplyFunction", "\u2061" }, + { "approx", "\u2248" }, + { "approxeq", "\u224A" }, + { "Aring", "\u00C5" }, + { "aring", "\u00E5" }, + { "Ascr", "\uD835\uDC9C" }, + { "ascr", "\uD835\uDCB6" }, + { "Assign", "\u2254" }, + { "ast", "\u002A" }, + { "asymp", "\u2248" }, + { "asympeq", "\u224D" }, + { "Atilde", "\u00C3" }, + { "atilde", "\u00E3" }, + { "Auml", "\u00C4" }, + { "auml", "\u00E4" }, + { "awconint", "\u2233" }, + { "awint", "\u2A11" }, + { "backcong", "\u224C" }, + { "backepsilon", "\u03F6" }, + { "backprime", "\u2035" }, + { "backsim", "\u223D" }, + { "backsimeq", "\u22CD" }, + { "Backslash", "\u2216" }, + { "Barv", "\u2AE7" }, + { "barvee", "\u22BD" }, + { "Barwed", "\u2306" }, + { "barwed", "\u2305" }, + { "barwedge", "\u2305" }, + { "bbrk", "\u23B5" }, + { "bbrktbrk", "\u23B6" }, + { "bcong", "\u224C" }, + { "Bcy", "\u0411" }, + { "bcy", "\u0431" }, + { "bdquo", "\u201E" }, + { "becaus", "\u2235" }, + { "Because", "\u2235" }, + { "because", "\u2235" }, + { "bemptyv", "\u29B0" }, + { "bepsi", "\u03F6" }, + { "bernou", "\u212C" }, + { "Bernoullis", "\u212C" }, + { "Beta", "\u0392" }, + { "beta", "\u03B2" }, + { "beth", "\u2136" }, + { "between", "\u226C" }, + { "Bfr", "\uD835\uDD05" }, + { "bfr", "\uD835\uDD1F" }, + { "bigcap", "\u22C2" }, + { "bigcirc", "\u25EF" }, + { "bigcup", "\u22C3" }, + { "bigodot", "\u2A00" }, + { "bigoplus", "\u2A01" }, + { "bigotimes", "\u2A02" }, + { "bigsqcup", "\u2A06" }, + { "bigstar", "\u2605" }, + { "bigtriangledown", "\u25BD" }, + { "bigtriangleup", "\u25B3" }, + { "biguplus", "\u2A04" }, + { "bigvee", "\u22C1" }, + { "bigwedge", "\u22C0" }, + { "bkarow", "\u290D" }, + { "blacklozenge", "\u29EB" }, + { "blacksquare", "\u25AA" }, + { "blacktriangle", "\u25B4" }, + { "blacktriangledown", "\u25BE" }, + { "blacktriangleleft", "\u25C2" }, + { "blacktriangleright", "\u25B8" }, + { "blank", "\u2423" }, + { "blk12", "\u2592" }, + { "blk14", "\u2591" }, + { "blk34", "\u2593" }, + { "block", "\u2588" }, + { "bne", "\u003D\u20E5" }, + { "bnequiv", "\u2261\u20E5" }, + { "bNot", "\u2AED" }, + { "bnot", "\u2310" }, + { "Bopf", "\uD835\uDD39" }, + { "bopf", "\uD835\uDD53" }, + { "bot", "\u22A5" }, + { "bottom", "\u22A5" }, + { "bowtie", "\u22C8" }, + { "boxbox", "\u29C9" }, + { "boxDL", "\u2557" }, + { "boxDl", "\u2556" }, + { "boxdL", "\u2555" }, + { "boxdl", "\u2510" }, + { "boxDR", "\u2554" }, + { "boxDr", "\u2553" }, + { "boxdR", "\u2552" }, + { "boxdr", "\u250C" }, + { "boxH", "\u2550" }, + { "boxh", "\u2500" }, + { "boxHD", "\u2566" }, + { "boxHd", "\u2564" }, + { "boxhD", "\u2565" }, + { "boxhd", "\u252C" }, + { "boxHU", "\u2569" }, + { "boxHu", "\u2567" }, + { "boxhU", "\u2568" }, + { "boxhu", "\u2534" }, + { "boxminus", "\u229F" }, + { "boxplus", "\u229E" }, + { "boxtimes", "\u22A0" }, + { "boxUL", "\u255D" }, + { "boxUl", "\u255C" }, + { "boxuL", "\u255B" }, + { "boxul", "\u2518" }, + { "boxUR", "\u255A" }, + { "boxUr", "\u2559" }, + { "boxuR", "\u2558" }, + { "boxur", "\u2514" }, + { "boxV", "\u2551" }, + { "boxv", "\u2502" }, + { "boxVH", "\u256C" }, + { "boxVh", "\u256B" }, + { "boxvH", "\u256A" }, + { "boxvh", "\u253C" }, + { "boxVL", "\u2563" }, + { "boxVl", "\u2562" }, + { "boxvL", "\u2561" }, + { "boxvl", "\u2524" }, + { "boxVR", "\u2560" }, + { "boxVr", "\u255F" }, + { "boxvR", "\u255E" }, + { "boxvr", "\u251C" }, + { "bprime", "\u2035" }, + { "Breve", "\u02D8" }, + { "breve", "\u02D8" }, + { "brvbar", "\u00A6" }, + { "Bscr", "\u212C" }, + { "bscr", "\uD835\uDCB7" }, + { "bsemi", "\u204F" }, + { "bsim", "\u223D" }, + { "bsime", "\u22CD" }, + { "bsol", "\u005C" }, + { "bsolb", "\u29C5" }, + { "bsolhsub", "\u27C8" }, + { "bull", "\u2022" }, + { "bullet", "\u2022" }, + { "bump", "\u224E" }, + { "bumpE", "\u2AAE" }, + { "bumpe", "\u224F" }, + { "Bumpeq", "\u224E" }, + { "bumpeq", "\u224F" }, + { "Cacute", "\u0106" }, + { "cacute", "\u0107" }, + { "Cap", "\u22D2" }, + { "cap", "\u2229" }, + { "capand", "\u2A44" }, + { "capbrcup", "\u2A49" }, + { "capcap", "\u2A4B" }, + { "capcup", "\u2A47" }, + { "capdot", "\u2A40" }, + { "CapitalDifferentialD", "\u2145" }, + { "caps", "\u2229\uFE00" }, + { "caret", "\u2041" }, + { "caron", "\u02C7" }, + { "Cayleys", "\u212D" }, + { "ccaps", "\u2A4D" }, + { "Ccaron", "\u010C" }, + { "ccaron", "\u010D" }, + { "Ccedil", "\u00C7" }, + { "ccedil", "\u00E7" }, + { "Ccirc", "\u0108" }, + { "ccirc", "\u0109" }, + { "Cconint", "\u2230" }, + { "ccups", "\u2A4C" }, + { "ccupssm", "\u2A50" }, + { "Cdot", "\u010A" }, + { "cdot", "\u010B" }, + { "cedil", "\u00B8" }, + { "Cedilla", "\u00B8" }, + { "cemptyv", "\u29B2" }, + { "cent", "\u00A2" }, + { "CenterDot", "\u00B7" }, + { "centerdot", "\u00B7" }, + { "Cfr", "\u212D" }, + { "cfr", "\uD835\uDD20" }, + { "CHcy", "\u0427" }, + { "chcy", "\u0447" }, + { "check", "\u2713" }, + { "checkmark", "\u2713" }, + { "Chi", "\u03A7" }, + { "chi", "\u03C7" }, + { "cir", "\u25CB" }, + { "circ", "\u02C6" }, + { "circeq", "\u2257" }, + { "circlearrowleft", "\u21BA" }, + { "circlearrowright", "\u21BB" }, + { "circledast", "\u229B" }, + { "circledcirc", "\u229A" }, + { "circleddash", "\u229D" }, + { "CircleDot", "\u2299" }, + { "circledR", "\u00AE" }, + { "circledS", "\u24C8" }, + { "CircleMinus", "\u2296" }, + { "CirclePlus", "\u2295" }, + { "CircleTimes", "\u2297" }, + { "cirE", "\u29C3" }, + { "cire", "\u2257" }, + { "cirfnint", "\u2A10" }, + { "cirmid", "\u2AEF" }, + { "cirscir", "\u29C2" }, + { "ClockwiseContourIntegral", "\u2232" }, + { "CloseCurlyDoubleQuote", "\u201D" }, + { "CloseCurlyQuote", "\u2019" }, + { "clubs", "\u2663" }, + { "clubsuit", "\u2663" }, + { "Colon", "\u2237" }, + { "colon", "\u003A" }, + { "Colone", "\u2A74" }, + { "colone", "\u2254" }, + { "coloneq", "\u2254" }, + { "comma", "\u002C" }, + { "commat", "\u0040" }, + { "comp", "\u2201" }, + { "compfn", "\u2218" }, + { "complement", "\u2201" }, + { "complexes", "\u2102" }, + { "cong", "\u2245" }, + { "congdot", "\u2A6D" }, + { "Congruent", "\u2261" }, + { "Conint", "\u222F" }, + { "conint", "\u222E" }, + { "ContourIntegral", "\u222E" }, + { "Copf", "\u2102" }, + { "copf", "\uD835\uDD54" }, + { "coprod", "\u2210" }, + { "Coproduct", "\u2210" }, + { "COPY", "\u00A9" }, + { "copy", "\u00A9" }, + { "copysr", "\u2117" }, + { "CounterClockwiseContourIntegral", "\u2233" }, + { "crarr", "\u21B5" }, + { "Cross", "\u2A2F" }, + { "cross", "\u2717" }, + { "Cscr", "\uD835\uDC9E" }, + { "cscr", "\uD835\uDCB8" }, + { "csub", "\u2ACF" }, + { "csube", "\u2AD1" }, + { "csup", "\u2AD0" }, + { "csupe", "\u2AD2" }, + { "ctdot", "\u22EF" }, + { "cudarrl", "\u2938" }, + { "cudarrr", "\u2935" }, + { "cuepr", "\u22DE" }, + { "cuesc", "\u22DF" }, + { "cularr", "\u21B6" }, + { "cularrp", "\u293D" }, + { "Cup", "\u22D3" }, + { "cup", "\u222A" }, + { "cupbrcap", "\u2A48" }, + { "CupCap", "\u224D" }, + { "cupcap", "\u2A46" }, + { "cupcup", "\u2A4A" }, + { "cupdot", "\u228D" }, + { "cupor", "\u2A45" }, + { "cups", "\u222A\uFE00" }, + { "curarr", "\u21B7" }, + { "curarrm", "\u293C" }, + { "curlyeqprec", "\u22DE" }, + { "curlyeqsucc", "\u22DF" }, + { "curlyvee", "\u22CE" }, + { "curlywedge", "\u22CF" }, + { "curren", "\u00A4" }, + { "curvearrowleft", "\u21B6" }, + { "curvearrowright", "\u21B7" }, + { "cuvee", "\u22CE" }, + { "cuwed", "\u22CF" }, + { "cwconint", "\u2232" }, + { "cwint", "\u2231" }, + { "cylcty", "\u232D" }, + { "Dagger", "\u2021" }, + { "dagger", "\u2020" }, + { "daleth", "\u2138" }, + { "Darr", "\u21A1" }, + { "dArr", "\u21D3" }, + { "darr", "\u2193" }, + { "dash", "\u2010" }, + { "Dashv", "\u2AE4" }, + { "dashv", "\u22A3" }, + { "dbkarow", "\u290F" }, + { "dblac", "\u02DD" }, + { "Dcaron", "\u010E" }, + { "dcaron", "\u010F" }, + { "Dcy", "\u0414" }, + { "dcy", "\u0434" }, + { "DD", "\u2145" }, + { "dd", "\u2146" }, + { "ddagger", "\u2021" }, + { "ddarr", "\u21CA" }, + { "DDotrahd", "\u2911" }, + { "ddotseq", "\u2A77" }, + { "deg", "\u00B0" }, + { "Del", "\u2207" }, + { "Delta", "\u0394" }, + { "delta", "\u03B4" }, + { "demptyv", "\u29B1" }, + { "dfisht", "\u297F" }, + { "Dfr", "\uD835\uDD07" }, + { "dfr", "\uD835\uDD21" }, + { "dHar", "\u2965" }, + { "dharl", "\u21C3" }, + { "dharr", "\u21C2" }, + { "DiacriticalAcute", "\u00B4" }, + { "DiacriticalDot", "\u02D9" }, + { "DiacriticalDoubleAcute", "\u02DD" }, + { "DiacriticalGrave", "\u0060" }, + { "DiacriticalTilde", "\u02DC" }, + { "diam", "\u22C4" }, + { "Diamond", "\u22C4" }, + { "diamond", "\u22C4" }, + { "diamondsuit", "\u2666" }, + { "diams", "\u2666" }, + { "die", "\u00A8" }, + { "DifferentialD", "\u2146" }, + { "digamma", "\u03DD" }, + { "disin", "\u22F2" }, + { "div", "\u00F7" }, + { "divide", "\u00F7" }, + { "divideontimes", "\u22C7" }, + { "divonx", "\u22C7" }, + { "DJcy", "\u0402" }, + { "djcy", "\u0452" }, + { "dlcorn", "\u231E" }, + { "dlcrop", "\u230D" }, + { "dollar", "\u0024" }, + { "Dopf", "\uD835\uDD3B" }, + { "dopf", "\uD835\uDD55" }, + { "Dot", "\u00A8" }, + { "dot", "\u02D9" }, + { "DotDot", "\u20DC" }, + { "doteq", "\u2250" }, + { "doteqdot", "\u2251" }, + { "DotEqual", "\u2250" }, + { "dotminus", "\u2238" }, + { "dotplus", "\u2214" }, + { "dotsquare", "\u22A1" }, + { "doublebarwedge", "\u2306" }, + { "DoubleContourIntegral", "\u222F" }, + { "DoubleDot", "\u00A8" }, + { "DoubleDownArrow", "\u21D3" }, + { "DoubleLeftArrow", "\u21D0" }, + { "DoubleLeftRightArrow", "\u21D4" }, + { "DoubleLeftTee", "\u2AE4" }, + { "DoubleLongLeftArrow", "\u27F8" }, + { "DoubleLongLeftRightArrow", "\u27FA" }, + { "DoubleLongRightArrow", "\u27F9" }, + { "DoubleRightArrow", "\u21D2" }, + { "DoubleRightTee", "\u22A8" }, + { "DoubleUpArrow", "\u21D1" }, + { "DoubleUpDownArrow", "\u21D5" }, + { "DoubleVerticalBar", "\u2225" }, + { "DownArrow", "\u2193" }, + { "Downarrow", "\u21D3" }, + { "downarrow", "\u2193" }, + { "DownArrowBar", "\u2913" }, + { "DownArrowUpArrow", "\u21F5" }, + { "DownBreve", "\u0311" }, + { "downdownarrows", "\u21CA" }, + { "downharpoonleft", "\u21C3" }, + { "downharpoonright", "\u21C2" }, + { "DownLeftRightVector", "\u2950" }, + { "DownLeftTeeVector", "\u295E" }, + { "DownLeftVector", "\u21BD" }, + { "DownLeftVectorBar", "\u2956" }, + { "DownRightTeeVector", "\u295F" }, + { "DownRightVector", "\u21C1" }, + { "DownRightVectorBar", "\u2957" }, + { "DownTee", "\u22A4" }, + { "DownTeeArrow", "\u21A7" }, + { "drbkarow", "\u2910" }, + { "drcorn", "\u231F" }, + { "drcrop", "\u230C" }, + { "Dscr", "\uD835\uDC9F" }, + { "dscr", "\uD835\uDCB9" }, + { "DScy", "\u0405" }, + { "dscy", "\u0455" }, + { "dsol", "\u29F6" }, + { "Dstrok", "\u0110" }, + { "dstrok", "\u0111" }, + { "dtdot", "\u22F1" }, + { "dtri", "\u25BF" }, + { "dtrif", "\u25BE" }, + { "duarr", "\u21F5" }, + { "duhar", "\u296F" }, + { "dwangle", "\u29A6" }, + { "DZcy", "\u040F" }, + { "dzcy", "\u045F" }, + { "dzigrarr", "\u27FF" }, + { "Eacute", "\u00C9" }, + { "eacute", "\u00E9" }, + { "easter", "\u2A6E" }, + { "Ecaron", "\u011A" }, + { "ecaron", "\u011B" }, + { "ecir", "\u2256" }, + { "Ecirc", "\u00CA" }, + { "ecirc", "\u00EA" }, + { "ecolon", "\u2255" }, + { "Ecy", "\u042D" }, + { "ecy", "\u044D" }, + { "eDDot", "\u2A77" }, + { "Edot", "\u0116" }, + { "eDot", "\u2251" }, + { "edot", "\u0117" }, + { "ee", "\u2147" }, + { "efDot", "\u2252" }, + { "Efr", "\uD835\uDD08" }, + { "efr", "\uD835\uDD22" }, + { "eg", "\u2A9A" }, + { "Egrave", "\u00C8" }, + { "egrave", "\u00E8" }, + { "egs", "\u2A96" }, + { "egsdot", "\u2A98" }, + { "el", "\u2A99" }, + { "Element", "\u2208" }, + { "elinters", "\u23E7" }, + { "ell", "\u2113" }, + { "els", "\u2A95" }, + { "elsdot", "\u2A97" }, + { "Emacr", "\u0112" }, + { "emacr", "\u0113" }, + { "empty", "\u2205" }, + { "emptyset", "\u2205" }, + { "EmptySmallSquare", "\u25FB" }, + { "emptyv", "\u2205" }, + { "EmptyVerySmallSquare", "\u25AB" }, + { "emsp", "\u2003" }, + { "emsp13", "\u2004" }, + { "emsp14", "\u2005" }, + { "ENG", "\u014A" }, + { "eng", "\u014B" }, + { "ensp", "\u2002" }, + { "Eogon", "\u0118" }, + { "eogon", "\u0119" }, + { "Eopf", "\uD835\uDD3C" }, + { "eopf", "\uD835\uDD56" }, + { "epar", "\u22D5" }, + { "eparsl", "\u29E3" }, + { "eplus", "\u2A71" }, + { "epsi", "\u03B5" }, + { "Epsilon", "\u0395" }, + { "epsilon", "\u03B5" }, + { "epsiv", "\u03F5" }, + { "eqcirc", "\u2256" }, + { "eqcolon", "\u2255" }, + { "eqsim", "\u2242" }, + { "eqslantgtr", "\u2A96" }, + { "eqslantless", "\u2A95" }, + { "Equal", "\u2A75" }, + { "equals", "\u003D" }, + { "EqualTilde", "\u2242" }, + { "equest", "\u225F" }, + { "Equilibrium", "\u21CC" }, + { "equiv", "\u2261" }, + { "equivDD", "\u2A78" }, + { "eqvparsl", "\u29E5" }, + { "erarr", "\u2971" }, + { "erDot", "\u2253" }, + { "Escr", "\u2130" }, + { "escr", "\u212F" }, + { "esdot", "\u2250" }, + { "Esim", "\u2A73" }, + { "esim", "\u2242" }, + { "Eta", "\u0397" }, + { "eta", "\u03B7" }, + { "ETH", "\u00D0" }, + { "eth", "\u00F0" }, + { "Euml", "\u00CB" }, + { "euml", "\u00EB" }, + { "euro", "\u20AC" }, + { "excl", "\u0021" }, + { "exist", "\u2203" }, + { "Exists", "\u2203" }, + { "expectation", "\u2130" }, + { "ExponentialE", "\u2147" }, + { "exponentiale", "\u2147" }, + { "fallingdotseq", "\u2252" }, + { "Fcy", "\u0424" }, + { "fcy", "\u0444" }, + { "female", "\u2640" }, + { "ffilig", "\uFB03" }, + { "fflig", "\uFB00" }, + { "ffllig", "\uFB04" }, + { "Ffr", "\uD835\uDD09" }, + { "ffr", "\uD835\uDD23" }, + { "filig", "\uFB01" }, + { "FilledSmallSquare", "\u25FC" }, + { "FilledVerySmallSquare", "\u25AA" }, + { "fjlig", "\u0066\u006A" }, + { "flat", "\u266D" }, + { "fllig", "\uFB02" }, + { "fltns", "\u25B1" }, + { "fnof", "\u0192" }, + { "Fopf", "\uD835\uDD3D" }, + { "fopf", "\uD835\uDD57" }, + { "ForAll", "\u2200" }, + { "forall", "\u2200" }, + { "fork", "\u22D4" }, + { "forkv", "\u2AD9" }, + { "Fouriertrf", "\u2131" }, + { "fpartint", "\u2A0D" }, + { "frac12", "\u00BD" }, + { "frac13", "\u2153" }, + { "frac14", "\u00BC" }, + { "frac15", "\u2155" }, + { "frac16", "\u2159" }, + { "frac18", "\u215B" }, + { "frac23", "\u2154" }, + { "frac25", "\u2156" }, + { "frac34", "\u00BE" }, + { "frac35", "\u2157" }, + { "frac38", "\u215C" }, + { "frac45", "\u2158" }, + { "frac56", "\u215A" }, + { "frac58", "\u215D" }, + { "frac78", "\u215E" }, + { "frasl", "\u2044" }, + { "frown", "\u2322" }, + { "Fscr", "\u2131" }, + { "fscr", "\uD835\uDCBB" }, + { "gacute", "\u01F5" }, + { "Gamma", "\u0393" }, + { "gamma", "\u03B3" }, + { "Gammad", "\u03DC" }, + { "gammad", "\u03DD" }, + { "gap", "\u2A86" }, + { "Gbreve", "\u011E" }, + { "gbreve", "\u011F" }, + { "Gcedil", "\u0122" }, + { "Gcirc", "\u011C" }, + { "gcirc", "\u011D" }, + { "Gcy", "\u0413" }, + { "gcy", "\u0433" }, + { "Gdot", "\u0120" }, + { "gdot", "\u0121" }, + { "gE", "\u2267" }, + { "ge", "\u2265" }, + { "gEl", "\u2A8C" }, + { "gel", "\u22DB" }, + { "geq", "\u2265" }, + { "geqq", "\u2267" }, + { "geqslant", "\u2A7E" }, + { "ges", "\u2A7E" }, + { "gescc", "\u2AA9" }, + { "gesdot", "\u2A80" }, + { "gesdoto", "\u2A82" }, + { "gesdotol", "\u2A84" }, + { "gesl", "\u22DB\uFE00" }, + { "gesles", "\u2A94" }, + { "Gfr", "\uD835\uDD0A" }, + { "gfr", "\uD835\uDD24" }, + { "Gg", "\u22D9" }, + { "gg", "\u226B" }, + { "ggg", "\u22D9" }, + { "gimel", "\u2137" }, + { "GJcy", "\u0403" }, + { "gjcy", "\u0453" }, + { "gl", "\u2277" }, + { "gla", "\u2AA5" }, + { "glE", "\u2A92" }, + { "glj", "\u2AA4" }, + { "gnap", "\u2A8A" }, + { "gnapprox", "\u2A8A" }, + { "gnE", "\u2269" }, + { "gne", "\u2A88" }, + { "gneq", "\u2A88" }, + { "gneqq", "\u2269" }, + { "gnsim", "\u22E7" }, + { "Gopf", "\uD835\uDD3E" }, + { "gopf", "\uD835\uDD58" }, + { "grave", "\u0060" }, + { "GreaterEqual", "\u2265" }, + { "GreaterEqualLess", "\u22DB" }, + { "GreaterFullEqual", "\u2267" }, + { "GreaterGreater", "\u2AA2" }, + { "GreaterLess", "\u2277" }, + { "GreaterSlantEqual", "\u2A7E" }, + { "GreaterTilde", "\u2273" }, + { "Gscr", "\uD835\uDCA2" }, + { "gscr", "\u210A" }, + { "gsim", "\u2273" }, + { "gsime", "\u2A8E" }, + { "gsiml", "\u2A90" }, + { "GT", "\u003E" }, + { "Gt", "\u226B" }, + { "gt", "\u003E" }, + { "gtcc", "\u2AA7" }, + { "gtcir", "\u2A7A" }, + { "gtdot", "\u22D7" }, + { "gtlPar", "\u2995" }, + { "gtquest", "\u2A7C" }, + { "gtrapprox", "\u2A86" }, + { "gtrarr", "\u2978" }, + { "gtrdot", "\u22D7" }, + { "gtreqless", "\u22DB" }, + { "gtreqqless", "\u2A8C" }, + { "gtrless", "\u2277" }, + { "gtrsim", "\u2273" }, + { "gvertneqq", "\u2269\uFE00" }, + { "gvnE", "\u2269\uFE00" }, + { "Hacek", "\u02C7" }, + { "hairsp", "\u200A" }, + { "half", "\u00BD" }, + { "hamilt", "\u210B" }, + { "HARDcy", "\u042A" }, + { "hardcy", "\u044A" }, + { "hArr", "\u21D4" }, + { "harr", "\u2194" }, + { "harrcir", "\u2948" }, + { "harrw", "\u21AD" }, + { "Hat", "\u005E" }, + { "hbar", "\u210F" }, + { "Hcirc", "\u0124" }, + { "hcirc", "\u0125" }, + { "hearts", "\u2665" }, + { "heartsuit", "\u2665" }, + { "hellip", "\u2026" }, + { "hercon", "\u22B9" }, + { "Hfr", "\u210C" }, + { "hfr", "\uD835\uDD25" }, + { "HilbertSpace", "\u210B" }, + { "hksearow", "\u2925" }, + { "hkswarow", "\u2926" }, + { "hoarr", "\u21FF" }, + { "homtht", "\u223B" }, + { "hookleftarrow", "\u21A9" }, + { "hookrightarrow", "\u21AA" }, + { "Hopf", "\u210D" }, + { "hopf", "\uD835\uDD59" }, + { "horbar", "\u2015" }, + { "HorizontalLine", "\u2500" }, + { "Hscr", "\u210B" }, + { "hscr", "\uD835\uDCBD" }, + { "hslash", "\u210F" }, + { "Hstrok", "\u0126" }, + { "hstrok", "\u0127" }, + { "HumpDownHump", "\u224E" }, + { "HumpEqual", "\u224F" }, + { "hybull", "\u2043" }, + { "hyphen", "\u2010" }, + { "Iacute", "\u00CD" }, + { "iacute", "\u00ED" }, + { "ic", "\u2063" }, + { "Icirc", "\u00CE" }, + { "icirc", "\u00EE" }, + { "Icy", "\u0418" }, + { "icy", "\u0438" }, + { "Idot", "\u0130" }, + { "IEcy", "\u0415" }, + { "iecy", "\u0435" }, + { "iexcl", "\u00A1" }, + { "iff", "\u21D4" }, + { "Ifr", "\u2111" }, + { "ifr", "\uD835\uDD26" }, + { "Igrave", "\u00CC" }, + { "igrave", "\u00EC" }, + { "ii", "\u2148" }, + { "iiiint", "\u2A0C" }, + { "iiint", "\u222D" }, + { "iinfin", "\u29DC" }, + { "iiota", "\u2129" }, + { "IJlig", "\u0132" }, + { "ijlig", "\u0133" }, + { "Im", "\u2111" }, + { "Imacr", "\u012A" }, + { "imacr", "\u012B" }, + { "image", "\u2111" }, + { "ImaginaryI", "\u2148" }, + { "imagline", "\u2110" }, + { "imagpart", "\u2111" }, + { "imath", "\u0131" }, + { "imof", "\u22B7" }, + { "imped", "\u01B5" }, + { "Implies", "\u21D2" }, + { "in", "\u2208" }, + { "incare", "\u2105" }, + { "infin", "\u221E" }, + { "infintie", "\u29DD" }, + { "inodot", "\u0131" }, + { "Int", "\u222C" }, + { "int", "\u222B" }, + { "intcal", "\u22BA" }, + { "integers", "\u2124" }, + { "Integral", "\u222B" }, + { "intercal", "\u22BA" }, + { "Intersection", "\u22C2" }, + { "intlarhk", "\u2A17" }, + { "intprod", "\u2A3C" }, + { "InvisibleComma", "\u2063" }, + { "InvisibleTimes", "\u2062" }, + { "IOcy", "\u0401" }, + { "iocy", "\u0451" }, + { "Iogon", "\u012E" }, + { "iogon", "\u012F" }, + { "Iopf", "\uD835\uDD40" }, + { "iopf", "\uD835\uDD5A" }, + { "Iota", "\u0399" }, + { "iota", "\u03B9" }, + { "iprod", "\u2A3C" }, + { "iquest", "\u00BF" }, + { "Iscr", "\u2110" }, + { "iscr", "\uD835\uDCBE" }, + { "isin", "\u2208" }, + { "isindot", "\u22F5" }, + { "isinE", "\u22F9" }, + { "isins", "\u22F4" }, + { "isinsv", "\u22F3" }, + { "isinv", "\u2208" }, + { "it", "\u2062" }, + { "Itilde", "\u0128" }, + { "itilde", "\u0129" }, + { "Iukcy", "\u0406" }, + { "iukcy", "\u0456" }, + { "Iuml", "\u00CF" }, + { "iuml", "\u00EF" }, + { "Jcirc", "\u0134" }, + { "jcirc", "\u0135" }, + { "Jcy", "\u0419" }, + { "jcy", "\u0439" }, + { "Jfr", "\uD835\uDD0D" }, + { "jfr", "\uD835\uDD27" }, + { "jmath", "\u0237" }, + { "Jopf", "\uD835\uDD41" }, + { "jopf", "\uD835\uDD5B" }, + { "Jscr", "\uD835\uDCA5" }, + { "jscr", "\uD835\uDCBF" }, + { "Jsercy", "\u0408" }, + { "jsercy", "\u0458" }, + { "Jukcy", "\u0404" }, + { "jukcy", "\u0454" }, + { "Kappa", "\u039A" }, + { "kappa", "\u03BA" }, + { "kappav", "\u03F0" }, + { "Kcedil", "\u0136" }, + { "kcedil", "\u0137" }, + { "Kcy", "\u041A" }, + { "kcy", "\u043A" }, + { "Kfr", "\uD835\uDD0E" }, + { "kfr", "\uD835\uDD28" }, + { "kgreen", "\u0138" }, + { "KHcy", "\u0425" }, + { "khcy", "\u0445" }, + { "KJcy", "\u040C" }, + { "kjcy", "\u045C" }, + { "Kopf", "\uD835\uDD42" }, + { "kopf", "\uD835\uDD5C" }, + { "Kscr", "\uD835\uDCA6" }, + { "kscr", "\uD835\uDCC0" }, + { "lAarr", "\u21DA" }, + { "Lacute", "\u0139" }, + { "lacute", "\u013A" }, + { "laemptyv", "\u29B4" }, + { "lagran", "\u2112" }, + { "Lambda", "\u039B" }, + { "lambda", "\u03BB" }, + { "Lang", "\u27EA" }, + { "lang", "\u27E8" }, + { "langd", "\u2991" }, + { "langle", "\u27E8" }, + { "lap", "\u2A85" }, + { "Laplacetrf", "\u2112" }, + { "laquo", "\u00AB" }, + { "Larr", "\u219E" }, + { "lArr", "\u21D0" }, + { "larr", "\u2190" }, + { "larrb", "\u21E4" }, + { "larrbfs", "\u291F" }, + { "larrfs", "\u291D" }, + { "larrhk", "\u21A9" }, + { "larrlp", "\u21AB" }, + { "larrpl", "\u2939" }, + { "larrsim", "\u2973" }, + { "larrtl", "\u21A2" }, + { "lat", "\u2AAB" }, + { "lAtail", "\u291B" }, + { "latail", "\u2919" }, + { "late", "\u2AAD" }, + { "lates", "\u2AAD\uFE00" }, + { "lBarr", "\u290E" }, + { "lbarr", "\u290C" }, + { "lbbrk", "\u2772" }, + { "lbrace", "\u007B" }, + { "lbrack", "\u005B" }, + { "lbrke", "\u298B" }, + { "lbrksld", "\u298F" }, + { "lbrkslu", "\u298D" }, + { "Lcaron", "\u013D" }, + { "lcaron", "\u013E" }, + { "Lcedil", "\u013B" }, + { "lcedil", "\u013C" }, + { "lceil", "\u2308" }, + { "lcub", "\u007B" }, + { "Lcy", "\u041B" }, + { "lcy", "\u043B" }, + { "ldca", "\u2936" }, + { "ldquo", "\u201C" }, + { "ldquor", "\u201E" }, + { "ldrdhar", "\u2967" }, + { "ldrushar", "\u294B" }, + { "ldsh", "\u21B2" }, + { "lE", "\u2266" }, + { "le", "\u2264" }, + { "LeftAngleBracket", "\u27E8" }, + { "LeftArrow", "\u2190" }, + { "Leftarrow", "\u21D0" }, + { "leftarrow", "\u2190" }, + { "LeftArrowBar", "\u21E4" }, + { "LeftArrowRightArrow", "\u21C6" }, + { "leftarrowtail", "\u21A2" }, + { "LeftCeiling", "\u2308" }, + { "LeftDoubleBracket", "\u27E6" }, + { "LeftDownTeeVector", "\u2961" }, + { "LeftDownVector", "\u21C3" }, + { "LeftDownVectorBar", "\u2959" }, + { "LeftFloor", "\u230A" }, + { "leftharpoondown", "\u21BD" }, + { "leftharpoonup", "\u21BC" }, + { "leftleftarrows", "\u21C7" }, + { "LeftRightArrow", "\u2194" }, + { "Leftrightarrow", "\u21D4" }, + { "leftrightarrow", "\u2194" }, + { "leftrightarrows", "\u21C6" }, + { "leftrightharpoons", "\u21CB" }, + { "leftrightsquigarrow", "\u21AD" }, + { "LeftRightVector", "\u294E" }, + { "LeftTee", "\u22A3" }, + { "LeftTeeArrow", "\u21A4" }, + { "LeftTeeVector", "\u295A" }, + { "leftthreetimes", "\u22CB" }, + { "LeftTriangle", "\u22B2" }, + { "LeftTriangleBar", "\u29CF" }, + { "LeftTriangleEqual", "\u22B4" }, + { "LeftUpDownVector", "\u2951" }, + { "LeftUpTeeVector", "\u2960" }, + { "LeftUpVector", "\u21BF" }, + { "LeftUpVectorBar", "\u2958" }, + { "LeftVector", "\u21BC" }, + { "LeftVectorBar", "\u2952" }, + { "lEg", "\u2A8B" }, + { "leg", "\u22DA" }, + { "leq", "\u2264" }, + { "leqq", "\u2266" }, + { "leqslant", "\u2A7D" }, + { "les", "\u2A7D" }, + { "lescc", "\u2AA8" }, + { "lesdot", "\u2A7F" }, + { "lesdoto", "\u2A81" }, + { "lesdotor", "\u2A83" }, + { "lesg", "\u22DA\uFE00" }, + { "lesges", "\u2A93" }, + { "lessapprox", "\u2A85" }, + { "lessdot", "\u22D6" }, + { "lesseqgtr", "\u22DA" }, + { "lesseqqgtr", "\u2A8B" }, + { "LessEqualGreater", "\u22DA" }, + { "LessFullEqual", "\u2266" }, + { "LessGreater", "\u2276" }, + { "lessgtr", "\u2276" }, + { "LessLess", "\u2AA1" }, + { "lesssim", "\u2272" }, + { "LessSlantEqual", "\u2A7D" }, + { "LessTilde", "\u2272" }, + { "lfisht", "\u297C" }, + { "lfloor", "\u230A" }, + { "Lfr", "\uD835\uDD0F" }, + { "lfr", "\uD835\uDD29" }, + { "lg", "\u2276" }, + { "lgE", "\u2A91" }, + { "lHar", "\u2962" }, + { "lhard", "\u21BD" }, + { "lharu", "\u21BC" }, + { "lharul", "\u296A" }, + { "lhblk", "\u2584" }, + { "LJcy", "\u0409" }, + { "ljcy", "\u0459" }, + { "Ll", "\u22D8" }, + { "ll", "\u226A" }, + { "llarr", "\u21C7" }, + { "llcorner", "\u231E" }, + { "Lleftarrow", "\u21DA" }, + { "llhard", "\u296B" }, + { "lltri", "\u25FA" }, + { "Lmidot", "\u013F" }, + { "lmidot", "\u0140" }, + { "lmoust", "\u23B0" }, + { "lmoustache", "\u23B0" }, + { "lnap", "\u2A89" }, + { "lnapprox", "\u2A89" }, + { "lnE", "\u2268" }, + { "lne", "\u2A87" }, + { "lneq", "\u2A87" }, + { "lneqq", "\u2268" }, + { "lnsim", "\u22E6" }, + { "loang", "\u27EC" }, + { "loarr", "\u21FD" }, + { "lobrk", "\u27E6" }, + { "LongLeftArrow", "\u27F5" }, + { "Longleftarrow", "\u27F8" }, + { "longleftarrow", "\u27F5" }, + { "LongLeftRightArrow", "\u27F7" }, + { "Longleftrightarrow", "\u27FA" }, + { "longleftrightarrow", "\u27F7" }, + { "longmapsto", "\u27FC" }, + { "LongRightArrow", "\u27F6" }, + { "Longrightarrow", "\u27F9" }, + { "longrightarrow", "\u27F6" }, + { "looparrowleft", "\u21AB" }, + { "looparrowright", "\u21AC" }, + { "lopar", "\u2985" }, + { "Lopf", "\uD835\uDD43" }, + { "lopf", "\uD835\uDD5D" }, + { "loplus", "\u2A2D" }, + { "lotimes", "\u2A34" }, + { "lowast", "\u2217" }, + { "lowbar", "\u005F" }, + { "LowerLeftArrow", "\u2199" }, + { "LowerRightArrow", "\u2198" }, + { "loz", "\u25CA" }, + { "lozenge", "\u25CA" }, + { "lozf", "\u29EB" }, + { "lpar", "\u0028" }, + { "lparlt", "\u2993" }, + { "lrarr", "\u21C6" }, + { "lrcorner", "\u231F" }, + { "lrhar", "\u21CB" }, + { "lrhard", "\u296D" }, + { "lrm", "\u200E" }, + { "lrtri", "\u22BF" }, + { "lsaquo", "\u2039" }, + { "Lscr", "\u2112" }, + { "lscr", "\uD835\uDCC1" }, + { "Lsh", "\u21B0" }, + { "lsh", "\u21B0" }, + { "lsim", "\u2272" }, + { "lsime", "\u2A8D" }, + { "lsimg", "\u2A8F" }, + { "lsqb", "\u005B" }, + { "lsquo", "\u2018" }, + { "lsquor", "\u201A" }, + { "Lstrok", "\u0141" }, + { "lstrok", "\u0142" }, + { "LT", "\u003C" }, + { "Lt", "\u226A" }, + { "lt", "\u003C" }, + { "ltcc", "\u2AA6" }, + { "ltcir", "\u2A79" }, + { "ltdot", "\u22D6" }, + { "lthree", "\u22CB" }, + { "ltimes", "\u22C9" }, + { "ltlarr", "\u2976" }, + { "ltquest", "\u2A7B" }, + { "ltri", "\u25C3" }, + { "ltrie", "\u22B4" }, + { "ltrif", "\u25C2" }, + { "ltrPar", "\u2996" }, + { "lurdshar", "\u294A" }, + { "luruhar", "\u2966" }, + { "lvertneqq", "\u2268\uFE00" }, + { "lvnE", "\u2268\uFE00" }, + { "macr", "\u00AF" }, + { "male", "\u2642" }, + { "malt", "\u2720" }, + { "maltese", "\u2720" }, + { "Map", "\u2905" }, + { "map", "\u21A6" }, + { "mapsto", "\u21A6" }, + { "mapstodown", "\u21A7" }, + { "mapstoleft", "\u21A4" }, + { "mapstoup", "\u21A5" }, + { "marker", "\u25AE" }, + { "mcomma", "\u2A29" }, + { "Mcy", "\u041C" }, + { "mcy", "\u043C" }, + { "mdash", "\u2014" }, + { "mDDot", "\u223A" }, + { "measuredangle", "\u2221" }, + { "MediumSpace", "\u205F" }, + { "Mellintrf", "\u2133" }, + { "Mfr", "\uD835\uDD10" }, + { "mfr", "\uD835\uDD2A" }, + { "mho", "\u2127" }, + { "micro", "\u00B5" }, + { "mid", "\u2223" }, + { "midast", "\u002A" }, + { "midcir", "\u2AF0" }, + { "middot", "\u00B7" }, + { "minus", "\u2212" }, + { "minusb", "\u229F" }, + { "minusd", "\u2238" }, + { "minusdu", "\u2A2A" }, + { "MinusPlus", "\u2213" }, + { "mlcp", "\u2ADB" }, + { "mldr", "\u2026" }, + { "mnplus", "\u2213" }, + { "models", "\u22A7" }, + { "Mopf", "\uD835\uDD44" }, + { "mopf", "\uD835\uDD5E" }, + { "mp", "\u2213" }, + { "Mscr", "\u2133" }, + { "mscr", "\uD835\uDCC2" }, + { "mstpos", "\u223E" }, + { "Mu", "\u039C" }, + { "mu", "\u03BC" }, + { "multimap", "\u22B8" }, + { "mumap", "\u22B8" }, + { "nabla", "\u2207" }, + { "Nacute", "\u0143" }, + { "nacute", "\u0144" }, + { "nang", "\u2220\u20D2" }, + { "nap", "\u2249" }, + { "napE", "\u2A70\u0338" }, + { "napid", "\u224B\u0338" }, + { "napos", "\u0149" }, + { "napprox", "\u2249" }, + { "natur", "\u266E" }, + { "natural", "\u266E" }, + { "naturals", "\u2115" }, + { "nbsp", "\u00A0" }, + { "nbump", "\u224E\u0338" }, + { "nbumpe", "\u224F\u0338" }, + { "ncap", "\u2A43" }, + { "Ncaron", "\u0147" }, + { "ncaron", "\u0148" }, + { "Ncedil", "\u0145" }, + { "ncedil", "\u0146" }, + { "ncong", "\u2247" }, + { "ncongdot", "\u2A6D\u0338" }, + { "ncup", "\u2A42" }, + { "Ncy", "\u041D" }, + { "ncy", "\u043D" }, + { "ndash", "\u2013" }, + { "ne", "\u2260" }, + { "nearhk", "\u2924" }, + { "neArr", "\u21D7" }, + { "nearr", "\u2197" }, + { "nearrow", "\u2197" }, + { "nedot", "\u2250\u0338" }, + { "NegativeMediumSpace", "\u200B" }, + { "NegativeThickSpace", "\u200B" }, + { "NegativeThinSpace", "\u200B" }, + { "NegativeVeryThinSpace", "\u200B" }, + { "nequiv", "\u2262" }, + { "nesear", "\u2928" }, + { "nesim", "\u2242\u0338" }, + { "NestedGreaterGreater", "\u226B" }, + { "NestedLessLess", "\u226A" }, + { "NewLine", "\u000A" }, + { "nexist", "\u2204" }, + { "nexists", "\u2204" }, + { "Nfr", "\uD835\uDD11" }, + { "nfr", "\uD835\uDD2B" }, + { "ngE", "\u2267\u0338" }, + { "nge", "\u2271" }, + { "ngeq", "\u2271" }, + { "ngeqq", "\u2267\u0338" }, + { "ngeqslant", "\u2A7E\u0338" }, + { "nges", "\u2A7E\u0338" }, + { "nGg", "\u22D9\u0338" }, + { "ngsim", "\u2275" }, + { "nGt", "\u226B\u20D2" }, + { "ngt", "\u226F" }, + { "ngtr", "\u226F" }, + { "nGtv", "\u226B\u0338" }, + { "nhArr", "\u21CE" }, + { "nharr", "\u21AE" }, + { "nhpar", "\u2AF2" }, + { "ni", "\u220B" }, + { "nis", "\u22FC" }, + { "nisd", "\u22FA" }, + { "niv", "\u220B" }, + { "NJcy", "\u040A" }, + { "njcy", "\u045A" }, + { "nlArr", "\u21CD" }, + { "nlarr", "\u219A" }, + { "nldr", "\u2025" }, + { "nlE", "\u2266\u0338" }, + { "nle", "\u2270" }, + { "nLeftarrow", "\u21CD" }, + { "nleftarrow", "\u219A" }, + { "nLeftrightarrow", "\u21CE" }, + { "nleftrightarrow", "\u21AE" }, + { "nleq", "\u2270" }, + { "nleqq", "\u2266\u0338" }, + { "nleqslant", "\u2A7D\u0338" }, + { "nles", "\u2A7D\u0338" }, + { "nless", "\u226E" }, + { "nLl", "\u22D8\u0338" }, + { "nlsim", "\u2274" }, + { "nLt", "\u226A\u20D2" }, + { "nlt", "\u226E" }, + { "nltri", "\u22EA" }, + { "nltrie", "\u22EC" }, + { "nLtv", "\u226A\u0338" }, + { "nmid", "\u2224" }, + { "NoBreak", "\u2060" }, + { "NonBreakingSpace", "\u00A0" }, + { "Nopf", "\u2115" }, + { "nopf", "\uD835\uDD5F" }, + { "Not", "\u2AEC" }, + { "not", "\u00AC" }, + { "NotCongruent", "\u2262" }, + { "NotCupCap", "\u226D" }, + { "NotDoubleVerticalBar", "\u2226" }, + { "NotElement", "\u2209" }, + { "NotEqual", "\u2260" }, + { "NotEqualTilde", "\u2242\u0338" }, + { "NotExists", "\u2204" }, + { "NotGreater", "\u226F" }, + { "NotGreaterEqual", "\u2271" }, + { "NotGreaterFullEqual", "\u2267\u0338" }, + { "NotGreaterGreater", "\u226B\u0338" }, + { "NotGreaterLess", "\u2279" }, + { "NotGreaterSlantEqual", "\u2A7E\u0338" }, + { "NotGreaterTilde", "\u2275" }, + { "NotHumpDownHump", "\u224E\u0338" }, + { "NotHumpEqual", "\u224F\u0338" }, + { "notin", "\u2209" }, + { "notindot", "\u22F5\u0338" }, + { "notinE", "\u22F9\u0338" }, + { "notinva", "\u2209" }, + { "notinvb", "\u22F7" }, + { "notinvc", "\u22F6" }, + { "NotLeftTriangle", "\u22EA" }, + { "NotLeftTriangleBar", "\u29CF\u0338" }, + { "NotLeftTriangleEqual", "\u22EC" }, + { "NotLess", "\u226E" }, + { "NotLessEqual", "\u2270" }, + { "NotLessGreater", "\u2278" }, + { "NotLessLess", "\u226A\u0338" }, + { "NotLessSlantEqual", "\u2A7D\u0338" }, + { "NotLessTilde", "\u2274" }, + { "NotNestedGreaterGreater", "\u2AA2\u0338" }, + { "NotNestedLessLess", "\u2AA1\u0338" }, + { "notni", "\u220C" }, + { "notniva", "\u220C" }, + { "notnivb", "\u22FE" }, + { "notnivc", "\u22FD" }, + { "NotPrecedes", "\u2280" }, + { "NotPrecedesEqual", "\u2AAF\u0338" }, + { "NotPrecedesSlantEqual", "\u22E0" }, + { "NotReverseElement", "\u220C" }, + { "NotRightTriangle", "\u22EB" }, + { "NotRightTriangleBar", "\u29D0\u0338" }, + { "NotRightTriangleEqual", "\u22ED" }, + { "NotSquareSubset", "\u228F\u0338" }, + { "NotSquareSubsetEqual", "\u22E2" }, + { "NotSquareSuperset", "\u2290\u0338" }, + { "NotSquareSupersetEqual", "\u22E3" }, + { "NotSubset", "\u2282\u20D2" }, + { "NotSubsetEqual", "\u2288" }, + { "NotSucceeds", "\u2281" }, + { "NotSucceedsEqual", "\u2AB0\u0338" }, + { "NotSucceedsSlantEqual", "\u22E1" }, + { "NotSucceedsTilde", "\u227F\u0338" }, + { "NotSuperset", "\u2283\u20D2" }, + { "NotSupersetEqual", "\u2289" }, + { "NotTilde", "\u2241" }, + { "NotTildeEqual", "\u2244" }, + { "NotTildeFullEqual", "\u2247" }, + { "NotTildeTilde", "\u2249" }, + { "NotVerticalBar", "\u2224" }, + { "npar", "\u2226" }, + { "nparallel", "\u2226" }, + { "nparsl", "\u2AFD\u20E5" }, + { "npart", "\u2202\u0338" }, + { "npolint", "\u2A14" }, + { "npr", "\u2280" }, + { "nprcue", "\u22E0" }, + { "npre", "\u2AAF\u0338" }, + { "nprec", "\u2280" }, + { "npreceq", "\u2AAF\u0338" }, + { "nrArr", "\u21CF" }, + { "nrarr", "\u219B" }, + { "nrarrc", "\u2933\u0338" }, + { "nrarrw", "\u219D\u0338" }, + { "nRightarrow", "\u21CF" }, + { "nrightarrow", "\u219B" }, + { "nrtri", "\u22EB" }, + { "nrtrie", "\u22ED" }, + { "nsc", "\u2281" }, + { "nsccue", "\u22E1" }, + { "nsce", "\u2AB0\u0338" }, + { "Nscr", "\uD835\uDCA9" }, + { "nscr", "\uD835\uDCC3" }, + { "nshortmid", "\u2224" }, + { "nshortparallel", "\u2226" }, + { "nsim", "\u2241" }, + { "nsime", "\u2244" }, + { "nsimeq", "\u2244" }, + { "nsmid", "\u2224" }, + { "nspar", "\u2226" }, + { "nsqsube", "\u22E2" }, + { "nsqsupe", "\u22E3" }, + { "nsub", "\u2284" }, + { "nsubE", "\u2AC5\u0338" }, + { "nsube", "\u2288" }, + { "nsubset", "\u2282\u20D2" }, + { "nsubseteq", "\u2288" }, + { "nsubseteqq", "\u2AC5\u0338" }, + { "nsucc", "\u2281" }, + { "nsucceq", "\u2AB0\u0338" }, + { "nsup", "\u2285" }, + { "nsupE", "\u2AC6\u0338" }, + { "nsupe", "\u2289" }, + { "nsupset", "\u2283\u20D2" }, + { "nsupseteq", "\u2289" }, + { "nsupseteqq", "\u2AC6\u0338" }, + { "ntgl", "\u2279" }, + { "Ntilde", "\u00D1" }, + { "ntilde", "\u00F1" }, + { "ntlg", "\u2278" }, + { "ntriangleleft", "\u22EA" }, + { "ntrianglelefteq", "\u22EC" }, + { "ntriangleright", "\u22EB" }, + { "ntrianglerighteq", "\u22ED" }, + { "Nu", "\u039D" }, + { "nu", "\u03BD" }, + { "num", "\u0023" }, + { "numero", "\u2116" }, + { "numsp", "\u2007" }, + { "nvap", "\u224D\u20D2" }, + { "nVDash", "\u22AF" }, + { "nVdash", "\u22AE" }, + { "nvDash", "\u22AD" }, + { "nvdash", "\u22AC" }, + { "nvge", "\u2265\u20D2" }, + { "nvgt", "\u003E\u20D2" }, + { "nvHarr", "\u2904" }, + { "nvinfin", "\u29DE" }, + { "nvlArr", "\u2902" }, + { "nvle", "\u2264\u20D2" }, + { "nvlt", "\u003C\u20D2" }, + { "nvltrie", "\u22B4\u20D2" }, + { "nvrArr", "\u2903" }, + { "nvrtrie", "\u22B5\u20D2" }, + { "nvsim", "\u223C\u20D2" }, + { "nwarhk", "\u2923" }, + { "nwArr", "\u21D6" }, + { "nwarr", "\u2196" }, + { "nwarrow", "\u2196" }, + { "nwnear", "\u2927" }, + { "Oacute", "\u00D3" }, + { "oacute", "\u00F3" }, + { "oast", "\u229B" }, + { "ocir", "\u229A" }, + { "Ocirc", "\u00D4" }, + { "ocirc", "\u00F4" }, + { "Ocy", "\u041E" }, + { "ocy", "\u043E" }, + { "odash", "\u229D" }, + { "Odblac", "\u0150" }, + { "odblac", "\u0151" }, + { "odiv", "\u2A38" }, + { "odot", "\u2299" }, + { "odsold", "\u29BC" }, + { "OElig", "\u0152" }, + { "oelig", "\u0153" }, + { "ofcir", "\u29BF" }, + { "Ofr", "\uD835\uDD12" }, + { "ofr", "\uD835\uDD2C" }, + { "ogon", "\u02DB" }, + { "Ograve", "\u00D2" }, + { "ograve", "\u00F2" }, + { "ogt", "\u29C1" }, + { "ohbar", "\u29B5" }, + { "ohm", "\u03A9" }, + { "oint", "\u222E" }, + { "olarr", "\u21BA" }, + { "olcir", "\u29BE" }, + { "olcross", "\u29BB" }, + { "oline", "\u203E" }, + { "olt", "\u29C0" }, + { "Omacr", "\u014C" }, + { "omacr", "\u014D" }, + { "Omega", "\u03A9" }, + { "omega", "\u03C9" }, + { "Omicron", "\u039F" }, + { "omicron", "\u03BF" }, + { "omid", "\u29B6" }, + { "ominus", "\u2296" }, + { "Oopf", "\uD835\uDD46" }, + { "oopf", "\uD835\uDD60" }, + { "opar", "\u29B7" }, + { "OpenCurlyDoubleQuote", "\u201C" }, + { "OpenCurlyQuote", "\u2018" }, + { "operp", "\u29B9" }, + { "oplus", "\u2295" }, + { "Or", "\u2A54" }, + { "or", "\u2228" }, + { "orarr", "\u21BB" }, + { "ord", "\u2A5D" }, + { "order", "\u2134" }, + { "orderof", "\u2134" }, + { "ordf", "\u00AA" }, + { "ordm", "\u00BA" }, + { "origof", "\u22B6" }, + { "oror", "\u2A56" }, + { "orslope", "\u2A57" }, + { "orv", "\u2A5B" }, + { "oS", "\u24C8" }, + { "Oscr", "\uD835\uDCAA" }, + { "oscr", "\u2134" }, + { "Oslash", "\u00D8" }, + { "oslash", "\u00F8" }, + { "osol", "\u2298" }, + { "Otilde", "\u00D5" }, + { "otilde", "\u00F5" }, + { "Otimes", "\u2A37" }, + { "otimes", "\u2297" }, + { "otimesas", "\u2A36" }, + { "Ouml", "\u00D6" }, + { "ouml", "\u00F6" }, + { "ovbar", "\u233D" }, + { "OverBar", "\u203E" }, + { "OverBrace", "\u23DE" }, + { "OverBracket", "\u23B4" }, + { "OverParenthesis", "\u23DC" }, + { "par", "\u2225" }, + { "para", "\u00B6" }, + { "parallel", "\u2225" }, + { "parsim", "\u2AF3" }, + { "parsl", "\u2AFD" }, + { "part", "\u2202" }, + { "PartialD", "\u2202" }, + { "Pcy", "\u041F" }, + { "pcy", "\u043F" }, + { "percnt", "\u0025" }, + { "period", "\u002E" }, + { "permil", "\u2030" }, + { "perp", "\u22A5" }, + { "pertenk", "\u2031" }, + { "Pfr", "\uD835\uDD13" }, + { "pfr", "\uD835\uDD2D" }, + { "Phi", "\u03A6" }, + { "phi", "\u03C6" }, + { "phiv", "\u03D5" }, + { "phmmat", "\u2133" }, + { "phone", "\u260E" }, + { "Pi", "\u03A0" }, + { "pi", "\u03C0" }, + { "pitchfork", "\u22D4" }, + { "piv", "\u03D6" }, + { "planck", "\u210F" }, + { "planckh", "\u210E" }, + { "plankv", "\u210F" }, + { "plus", "\u002B" }, + { "plusacir", "\u2A23" }, + { "plusb", "\u229E" }, + { "pluscir", "\u2A22" }, + { "plusdo", "\u2214" }, + { "plusdu", "\u2A25" }, + { "pluse", "\u2A72" }, + { "PlusMinus", "\u00B1" }, + { "plusmn", "\u00B1" }, + { "plussim", "\u2A26" }, + { "plustwo", "\u2A27" }, + { "pm", "\u00B1" }, + { "Poincareplane", "\u210C" }, + { "pointint", "\u2A15" }, + { "Popf", "\u2119" }, + { "popf", "\uD835\uDD61" }, + { "pound", "\u00A3" }, + { "Pr", "\u2ABB" }, + { "pr", "\u227A" }, + { "prap", "\u2AB7" }, + { "prcue", "\u227C" }, + { "prE", "\u2AB3" }, + { "pre", "\u2AAF" }, + { "prec", "\u227A" }, + { "precapprox", "\u2AB7" }, + { "preccurlyeq", "\u227C" }, + { "Precedes", "\u227A" }, + { "PrecedesEqual", "\u2AAF" }, + { "PrecedesSlantEqual", "\u227C" }, + { "PrecedesTilde", "\u227E" }, + { "preceq", "\u2AAF" }, + { "precnapprox", "\u2AB9" }, + { "precneqq", "\u2AB5" }, + { "precnsim", "\u22E8" }, + { "precsim", "\u227E" }, + { "Prime", "\u2033" }, + { "prime", "\u2032" }, + { "primes", "\u2119" }, + { "prnap", "\u2AB9" }, + { "prnE", "\u2AB5" }, + { "prnsim", "\u22E8" }, + { "prod", "\u220F" }, + { "Product", "\u220F" }, + { "profalar", "\u232E" }, + { "profline", "\u2312" }, + { "profsurf", "\u2313" }, + { "prop", "\u221D" }, + { "Proportion", "\u2237" }, + { "Proportional", "\u221D" }, + { "propto", "\u221D" }, + { "prsim", "\u227E" }, + { "prurel", "\u22B0" }, + { "Pscr", "\uD835\uDCAB" }, + { "pscr", "\uD835\uDCC5" }, + { "Psi", "\u03A8" }, + { "psi", "\u03C8" }, + { "puncsp", "\u2008" }, + { "Qfr", "\uD835\uDD14" }, + { "qfr", "\uD835\uDD2E" }, + { "qint", "\u2A0C" }, + { "Qopf", "\u211A" }, + { "qopf", "\uD835\uDD62" }, + { "qprime", "\u2057" }, + { "Qscr", "\uD835\uDCAC" }, + { "qscr", "\uD835\uDCC6" }, + { "quaternions", "\u210D" }, + { "quatint", "\u2A16" }, + { "quest", "\u003F" }, + { "questeq", "\u225F" }, + { "QUOT", "\u0022" }, + { "quot", "\u0022" }, + { "rAarr", "\u21DB" }, + { "race", "\u223D\u0331" }, + { "Racute", "\u0154" }, + { "racute", "\u0155" }, + { "radic", "\u221A" }, + { "raemptyv", "\u29B3" }, + { "Rang", "\u27EB" }, + { "rang", "\u27E9" }, + { "rangd", "\u2992" }, + { "range", "\u29A5" }, + { "rangle", "\u27E9" }, + { "raquo", "\u00BB" }, + { "Rarr", "\u21A0" }, + { "rArr", "\u21D2" }, + { "rarr", "\u2192" }, + { "rarrap", "\u2975" }, + { "rarrb", "\u21E5" }, + { "rarrbfs", "\u2920" }, + { "rarrc", "\u2933" }, + { "rarrfs", "\u291E" }, + { "rarrhk", "\u21AA" }, + { "rarrlp", "\u21AC" }, + { "rarrpl", "\u2945" }, + { "rarrsim", "\u2974" }, + { "Rarrtl", "\u2916" }, + { "rarrtl", "\u21A3" }, + { "rarrw", "\u219D" }, + { "rAtail", "\u291C" }, + { "ratail", "\u291A" }, + { "ratio", "\u2236" }, + { "rationals", "\u211A" }, + { "RBarr", "\u2910" }, + { "rBarr", "\u290F" }, + { "rbarr", "\u290D" }, + { "rbbrk", "\u2773" }, + { "rbrace", "\u007D" }, + { "rbrack", "\u005D" }, + { "rbrke", "\u298C" }, + { "rbrksld", "\u298E" }, + { "rbrkslu", "\u2990" }, + { "Rcaron", "\u0158" }, + { "rcaron", "\u0159" }, + { "Rcedil", "\u0156" }, + { "rcedil", "\u0157" }, + { "rceil", "\u2309" }, + { "rcub", "\u007D" }, + { "Rcy", "\u0420" }, + { "rcy", "\u0440" }, + { "rdca", "\u2937" }, + { "rdldhar", "\u2969" }, + { "rdquo", "\u201D" }, + { "rdquor", "\u201D" }, + { "rdsh", "\u21B3" }, + { "Re", "\u211C" }, + { "real", "\u211C" }, + { "realine", "\u211B" }, + { "realpart", "\u211C" }, + { "reals", "\u211D" }, + { "rect", "\u25AD" }, + { "REG", "\u00AE" }, + { "reg", "\u00AE" }, + { "ReverseElement", "\u220B" }, + { "ReverseEquilibrium", "\u21CB" }, + { "ReverseUpEquilibrium", "\u296F" }, + { "rfisht", "\u297D" }, + { "rfloor", "\u230B" }, + { "Rfr", "\u211C" }, + { "rfr", "\uD835\uDD2F" }, + { "rHar", "\u2964" }, + { "rhard", "\u21C1" }, + { "rharu", "\u21C0" }, + { "rharul", "\u296C" }, + { "Rho", "\u03A1" }, + { "rho", "\u03C1" }, + { "rhov", "\u03F1" }, + { "RightAngleBracket", "\u27E9" }, + { "RightArrow", "\u2192" }, + { "Rightarrow", "\u21D2" }, + { "rightarrow", "\u2192" }, + { "RightArrowBar", "\u21E5" }, + { "RightArrowLeftArrow", "\u21C4" }, + { "rightarrowtail", "\u21A3" }, + { "RightCeiling", "\u2309" }, + { "RightDoubleBracket", "\u27E7" }, + { "RightDownTeeVector", "\u295D" }, + { "RightDownVector", "\u21C2" }, + { "RightDownVectorBar", "\u2955" }, + { "RightFloor", "\u230B" }, + { "rightharpoondown", "\u21C1" }, + { "rightharpoonup", "\u21C0" }, + { "rightleftarrows", "\u21C4" }, + { "rightleftharpoons", "\u21CC" }, + { "rightrightarrows", "\u21C9" }, + { "rightsquigarrow", "\u219D" }, + { "RightTee", "\u22A2" }, + { "RightTeeArrow", "\u21A6" }, + { "RightTeeVector", "\u295B" }, + { "rightthreetimes", "\u22CC" }, + { "RightTriangle", "\u22B3" }, + { "RightTriangleBar", "\u29D0" }, + { "RightTriangleEqual", "\u22B5" }, + { "RightUpDownVector", "\u294F" }, + { "RightUpTeeVector", "\u295C" }, + { "RightUpVector", "\u21BE" }, + { "RightUpVectorBar", "\u2954" }, + { "RightVector", "\u21C0" }, + { "RightVectorBar", "\u2953" }, + { "ring", "\u02DA" }, + { "risingdotseq", "\u2253" }, + { "rlarr", "\u21C4" }, + { "rlhar", "\u21CC" }, + { "rlm", "\u200F" }, + { "rmoust", "\u23B1" }, + { "rmoustache", "\u23B1" }, + { "rnmid", "\u2AEE" }, + { "roang", "\u27ED" }, + { "roarr", "\u21FE" }, + { "robrk", "\u27E7" }, + { "ropar", "\u2986" }, + { "Ropf", "\u211D" }, + { "ropf", "\uD835\uDD63" }, + { "roplus", "\u2A2E" }, + { "rotimes", "\u2A35" }, + { "RoundImplies", "\u2970" }, + { "rpar", "\u0029" }, + { "rpargt", "\u2994" }, + { "rppolint", "\u2A12" }, + { "rrarr", "\u21C9" }, + { "Rrightarrow", "\u21DB" }, + { "rsaquo", "\u203A" }, + { "Rscr", "\u211B" }, + { "rscr", "\uD835\uDCC7" }, + { "Rsh", "\u21B1" }, + { "rsh", "\u21B1" }, + { "rsqb", "\u005D" }, + { "rsquo", "\u2019" }, + { "rsquor", "\u2019" }, + { "rthree", "\u22CC" }, + { "rtimes", "\u22CA" }, + { "rtri", "\u25B9" }, + { "rtrie", "\u22B5" }, + { "rtrif", "\u25B8" }, + { "rtriltri", "\u29CE" }, + { "RuleDelayed", "\u29F4" }, + { "ruluhar", "\u2968" }, + { "rx", "\u211E" }, + { "Sacute", "\u015A" }, + { "sacute", "\u015B" }, + { "sbquo", "\u201A" }, + { "Sc", "\u2ABC" }, + { "sc", "\u227B" }, + { "scap", "\u2AB8" }, + { "Scaron", "\u0160" }, + { "scaron", "\u0161" }, + { "sccue", "\u227D" }, + { "scE", "\u2AB4" }, + { "sce", "\u2AB0" }, + { "Scedil", "\u015E" }, + { "scedil", "\u015F" }, + { "Scirc", "\u015C" }, + { "scirc", "\u015D" }, + { "scnap", "\u2ABA" }, + { "scnE", "\u2AB6" }, + { "scnsim", "\u22E9" }, + { "scpolint", "\u2A13" }, + { "scsim", "\u227F" }, + { "Scy", "\u0421" }, + { "scy", "\u0441" }, + { "sdot", "\u22C5" }, + { "sdotb", "\u22A1" }, + { "sdote", "\u2A66" }, + { "searhk", "\u2925" }, + { "seArr", "\u21D8" }, + { "searr", "\u2198" }, + { "searrow", "\u2198" }, + { "sect", "\u00A7" }, + { "semi", "\u003B" }, + { "seswar", "\u2929" }, + { "setminus", "\u2216" }, + { "setmn", "\u2216" }, + { "sext", "\u2736" }, + { "Sfr", "\uD835\uDD16" }, + { "sfr", "\uD835\uDD30" }, + { "sfrown", "\u2322" }, + { "sharp", "\u266F" }, + { "SHCHcy", "\u0429" }, + { "shchcy", "\u0449" }, + { "SHcy", "\u0428" }, + { "shcy", "\u0448" }, + { "ShortDownArrow", "\u2193" }, + { "ShortLeftArrow", "\u2190" }, + { "shortmid", "\u2223" }, + { "shortparallel", "\u2225" }, + { "ShortRightArrow", "\u2192" }, + { "ShortUpArrow", "\u2191" }, + { "shy", "\u00AD" }, + { "Sigma", "\u03A3" }, + { "sigma", "\u03C3" }, + { "sigmaf", "\u03C2" }, + { "sigmav", "\u03C2" }, + { "sim", "\u223C" }, + { "simdot", "\u2A6A" }, + { "sime", "\u2243" }, + { "simeq", "\u2243" }, + { "simg", "\u2A9E" }, + { "simgE", "\u2AA0" }, + { "siml", "\u2A9D" }, + { "simlE", "\u2A9F" }, + { "simne", "\u2246" }, + { "simplus", "\u2A24" }, + { "simrarr", "\u2972" }, + { "slarr", "\u2190" }, + { "SmallCircle", "\u2218" }, + { "smallsetminus", "\u2216" }, + { "smashp", "\u2A33" }, + { "smeparsl", "\u29E4" }, + { "smid", "\u2223" }, + { "smile", "\u2323" }, + { "smt", "\u2AAA" }, + { "smte", "\u2AAC" }, + { "smtes", "\u2AAC\uFE00" }, + { "SOFTcy", "\u042C" }, + { "softcy", "\u044C" }, + { "sol", "\u002F" }, + { "solb", "\u29C4" }, + { "solbar", "\u233F" }, + { "Sopf", "\uD835\uDD4A" }, + { "sopf", "\uD835\uDD64" }, + { "spades", "\u2660" }, + { "spadesuit", "\u2660" }, + { "spar", "\u2225" }, + { "sqcap", "\u2293" }, + { "sqcaps", "\u2293\uFE00" }, + { "sqcup", "\u2294" }, + { "sqcups", "\u2294\uFE00" }, + { "Sqrt", "\u221A" }, + { "sqsub", "\u228F" }, + { "sqsube", "\u2291" }, + { "sqsubset", "\u228F" }, + { "sqsubseteq", "\u2291" }, + { "sqsup", "\u2290" }, + { "sqsupe", "\u2292" }, + { "sqsupset", "\u2290" }, + { "sqsupseteq", "\u2292" }, + { "squ", "\u25A1" }, + { "Square", "\u25A1" }, + { "square", "\u25A1" }, + { "SquareIntersection", "\u2293" }, + { "SquareSubset", "\u228F" }, + { "SquareSubsetEqual", "\u2291" }, + { "SquareSuperset", "\u2290" }, + { "SquareSupersetEqual", "\u2292" }, + { "SquareUnion", "\u2294" }, + { "squarf", "\u25AA" }, + { "squf", "\u25AA" }, + { "srarr", "\u2192" }, + { "Sscr", "\uD835\uDCAE" }, + { "sscr", "\uD835\uDCC8" }, + { "ssetmn", "\u2216" }, + { "ssmile", "\u2323" }, + { "sstarf", "\u22C6" }, + { "Star", "\u22C6" }, + { "star", "\u2606" }, + { "starf", "\u2605" }, + { "straightepsilon", "\u03F5" }, + { "straightphi", "\u03D5" }, + { "strns", "\u00AF" }, + { "Sub", "\u22D0" }, + { "sub", "\u2282" }, + { "subdot", "\u2ABD" }, + { "subE", "\u2AC5" }, + { "sube", "\u2286" }, + { "subedot", "\u2AC3" }, + { "submult", "\u2AC1" }, + { "subnE", "\u2ACB" }, + { "subne", "\u228A" }, + { "subplus", "\u2ABF" }, + { "subrarr", "\u2979" }, + { "Subset", "\u22D0" }, + { "subset", "\u2282" }, + { "subseteq", "\u2286" }, + { "subseteqq", "\u2AC5" }, + { "SubsetEqual", "\u2286" }, + { "subsetneq", "\u228A" }, + { "subsetneqq", "\u2ACB" }, + { "subsim", "\u2AC7" }, + { "subsub", "\u2AD5" }, + { "subsup", "\u2AD3" }, + { "succ", "\u227B" }, + { "succapprox", "\u2AB8" }, + { "succcurlyeq", "\u227D" }, + { "Succeeds", "\u227B" }, + { "SucceedsEqual", "\u2AB0" }, + { "SucceedsSlantEqual", "\u227D" }, + { "SucceedsTilde", "\u227F" }, + { "succeq", "\u2AB0" }, + { "succnapprox", "\u2ABA" }, + { "succneqq", "\u2AB6" }, + { "succnsim", "\u22E9" }, + { "succsim", "\u227F" }, + { "SuchThat", "\u220B" }, + { "Sum", "\u2211" }, + { "sum", "\u2211" }, + { "sung", "\u266A" }, + { "Sup", "\u22D1" }, + { "sup", "\u2283" }, + { "sup1", "\u00B9" }, + { "sup2", "\u00B2" }, + { "sup3", "\u00B3" }, + { "supdot", "\u2ABE" }, + { "supdsub", "\u2AD8" }, + { "supE", "\u2AC6" }, + { "supe", "\u2287" }, + { "supedot", "\u2AC4" }, + { "Superset", "\u2283" }, + { "SupersetEqual", "\u2287" }, + { "suphsol", "\u27C9" }, + { "suphsub", "\u2AD7" }, + { "suplarr", "\u297B" }, + { "supmult", "\u2AC2" }, + { "supnE", "\u2ACC" }, + { "supne", "\u228B" }, + { "supplus", "\u2AC0" }, + { "Supset", "\u22D1" }, + { "supset", "\u2283" }, + { "supseteq", "\u2287" }, + { "supseteqq", "\u2AC6" }, + { "supsetneq", "\u228B" }, + { "supsetneqq", "\u2ACC" }, + { "supsim", "\u2AC8" }, + { "supsub", "\u2AD4" }, + { "supsup", "\u2AD6" }, + { "swarhk", "\u2926" }, + { "swArr", "\u21D9" }, + { "swarr", "\u2199" }, + { "swarrow", "\u2199" }, + { "swnwar", "\u292A" }, + { "szlig", "\u00DF" }, + { "Tab", "\u0009" }, + { "target", "\u2316" }, + { "Tau", "\u03A4" }, + { "tau", "\u03C4" }, + { "tbrk", "\u23B4" }, + { "Tcaron", "\u0164" }, + { "tcaron", "\u0165" }, + { "Tcedil", "\u0162" }, + { "tcedil", "\u0163" }, + { "Tcy", "\u0422" }, + { "tcy", "\u0442" }, + { "tdot", "\u20DB" }, + { "telrec", "\u2315" }, + { "Tfr", "\uD835\uDD17" }, + { "tfr", "\uD835\uDD31" }, + { "there4", "\u2234" }, + { "Therefore", "\u2234" }, + { "therefore", "\u2234" }, + { "Theta", "\u0398" }, + { "theta", "\u03B8" }, + { "thetasym", "\u03D1" }, + { "thetav", "\u03D1" }, + { "thickapprox", "\u2248" }, + { "thicksim", "\u223C" }, + { "ThickSpace", "\u205F\u200A" }, + { "thinsp", "\u2009" }, + { "ThinSpace", "\u2009" }, + { "thkap", "\u2248" }, + { "thksim", "\u223C" }, + { "THORN", "\u00DE" }, + { "thorn", "\u00FE" }, + { "Tilde", "\u223C" }, + { "tilde", "\u02DC" }, + { "TildeEqual", "\u2243" }, + { "TildeFullEqual", "\u2245" }, + { "TildeTilde", "\u2248" }, + { "times", "\u00D7" }, + { "timesb", "\u22A0" }, + { "timesbar", "\u2A31" }, + { "timesd", "\u2A30" }, + { "tint", "\u222D" }, + { "toea", "\u2928" }, + { "top", "\u22A4" }, + { "topbot", "\u2336" }, + { "topcir", "\u2AF1" }, + { "Topf", "\uD835\uDD4B" }, + { "topf", "\uD835\uDD65" }, + { "topfork", "\u2ADA" }, + { "tosa", "\u2929" }, + { "tprime", "\u2034" }, + { "TRADE", "\u2122" }, + { "trade", "\u2122" }, + { "triangle", "\u25B5" }, + { "triangledown", "\u25BF" }, + { "triangleleft", "\u25C3" }, + { "trianglelefteq", "\u22B4" }, + { "triangleq", "\u225C" }, + { "triangleright", "\u25B9" }, + { "trianglerighteq", "\u22B5" }, + { "tridot", "\u25EC" }, + { "trie", "\u225C" }, + { "triminus", "\u2A3A" }, + { "TripleDot", "\u20DB" }, + { "triplus", "\u2A39" }, + { "trisb", "\u29CD" }, + { "tritime", "\u2A3B" }, + { "trpezium", "\u23E2" }, + { "Tscr", "\uD835\uDCAF" }, + { "tscr", "\uD835\uDCC9" }, + { "TScy", "\u0426" }, + { "tscy", "\u0446" }, + { "TSHcy", "\u040B" }, + { "tshcy", "\u045B" }, + { "Tstrok", "\u0166" }, + { "tstrok", "\u0167" }, + { "twixt", "\u226C" }, + { "twoheadleftarrow", "\u219E" }, + { "twoheadrightarrow", "\u21A0" }, + { "Uacute", "\u00DA" }, + { "uacute", "\u00FA" }, + { "Uarr", "\u219F" }, + { "uArr", "\u21D1" }, + { "uarr", "\u2191" }, + { "Uarrocir", "\u2949" }, + { "Ubrcy", "\u040E" }, + { "ubrcy", "\u045E" }, + { "Ubreve", "\u016C" }, + { "ubreve", "\u016D" }, + { "Ucirc", "\u00DB" }, + { "ucirc", "\u00FB" }, + { "Ucy", "\u0423" }, + { "ucy", "\u0443" }, + { "udarr", "\u21C5" }, + { "Udblac", "\u0170" }, + { "udblac", "\u0171" }, + { "udhar", "\u296E" }, + { "ufisht", "\u297E" }, + { "Ufr", "\uD835\uDD18" }, + { "ufr", "\uD835\uDD32" }, + { "Ugrave", "\u00D9" }, + { "ugrave", "\u00F9" }, + { "uHar", "\u2963" }, + { "uharl", "\u21BF" }, + { "uharr", "\u21BE" }, + { "uhblk", "\u2580" }, + { "ulcorn", "\u231C" }, + { "ulcorner", "\u231C" }, + { "ulcrop", "\u230F" }, + { "ultri", "\u25F8" }, + { "Umacr", "\u016A" }, + { "umacr", "\u016B" }, + { "uml", "\u00A8" }, + { "UnderBar", "\u005F" }, + { "UnderBrace", "\u23DF" }, + { "UnderBracket", "\u23B5" }, + { "UnderParenthesis", "\u23DD" }, + { "Union", "\u22C3" }, + { "UnionPlus", "\u228E" }, + { "Uogon", "\u0172" }, + { "uogon", "\u0173" }, + { "Uopf", "\uD835\uDD4C" }, + { "uopf", "\uD835\uDD66" }, + { "UpArrow", "\u2191" }, + { "Uparrow", "\u21D1" }, + { "uparrow", "\u2191" }, + { "UpArrowBar", "\u2912" }, + { "UpArrowDownArrow", "\u21C5" }, + { "UpDownArrow", "\u2195" }, + { "Updownarrow", "\u21D5" }, + { "updownarrow", "\u2195" }, + { "UpEquilibrium", "\u296E" }, + { "upharpoonleft", "\u21BF" }, + { "upharpoonright", "\u21BE" }, + { "uplus", "\u228E" }, + { "UpperLeftArrow", "\u2196" }, + { "UpperRightArrow", "\u2197" }, + { "Upsi", "\u03D2" }, + { "upsi", "\u03C5" }, + { "upsih", "\u03D2" }, + { "Upsilon", "\u03A5" }, + { "upsilon", "\u03C5" }, + { "UpTee", "\u22A5" }, + { "UpTeeArrow", "\u21A5" }, + { "upuparrows", "\u21C8" }, + { "urcorn", "\u231D" }, + { "urcorner", "\u231D" }, + { "urcrop", "\u230E" }, + { "Uring", "\u016E" }, + { "uring", "\u016F" }, + { "urtri", "\u25F9" }, + { "Uscr", "\uD835\uDCB0" }, + { "uscr", "\uD835\uDCCA" }, + { "utdot", "\u22F0" }, + { "Utilde", "\u0168" }, + { "utilde", "\u0169" }, + { "utri", "\u25B5" }, + { "utrif", "\u25B4" }, + { "uuarr", "\u21C8" }, + { "Uuml", "\u00DC" }, + { "uuml", "\u00FC" }, + { "uwangle", "\u29A7" }, + { "vangrt", "\u299C" }, + { "varepsilon", "\u03F5" }, + { "varkappa", "\u03F0" }, + { "varnothing", "\u2205" }, + { "varphi", "\u03D5" }, + { "varpi", "\u03D6" }, + { "varpropto", "\u221D" }, + { "vArr", "\u21D5" }, + { "varr", "\u2195" }, + { "varrho", "\u03F1" }, + { "varsigma", "\u03C2" }, + { "varsubsetneq", "\u228A\uFE00" }, + { "varsubsetneqq", "\u2ACB\uFE00" }, + { "varsupsetneq", "\u228B\uFE00" }, + { "varsupsetneqq", "\u2ACC\uFE00" }, + { "vartheta", "\u03D1" }, + { "vartriangleleft", "\u22B2" }, + { "vartriangleright", "\u22B3" }, + { "Vbar", "\u2AEB" }, + { "vBar", "\u2AE8" }, + { "vBarv", "\u2AE9" }, + { "Vcy", "\u0412" }, + { "vcy", "\u0432" }, + { "VDash", "\u22AB" }, + { "Vdash", "\u22A9" }, + { "vDash", "\u22A8" }, + { "vdash", "\u22A2" }, + { "Vdashl", "\u2AE6" }, + { "Vee", "\u22C1" }, + { "vee", "\u2228" }, + { "veebar", "\u22BB" }, + { "veeeq", "\u225A" }, + { "vellip", "\u22EE" }, + { "Verbar", "\u2016" }, + { "verbar", "\u007C" }, + { "Vert", "\u2016" }, + { "vert", "\u007C" }, + { "VerticalBar", "\u2223" }, + { "VerticalLine", "\u007C" }, + { "VerticalSeparator", "\u2758" }, + { "VerticalTilde", "\u2240" }, + { "VeryThinSpace", "\u200A" }, + { "Vfr", "\uD835\uDD19" }, + { "vfr", "\uD835\uDD33" }, + { "vltri", "\u22B2" }, + { "vnsub", "\u2282\u20D2" }, + { "vnsup", "\u2283\u20D2" }, + { "Vopf", "\uD835\uDD4D" }, + { "vopf", "\uD835\uDD67" }, + { "vprop", "\u221D" }, + { "vrtri", "\u22B3" }, + { "Vscr", "\uD835\uDCB1" }, + { "vscr", "\uD835\uDCCB" }, + { "vsubnE", "\u2ACB\uFE00" }, + { "vsubne", "\u228A\uFE00" }, + { "vsupnE", "\u2ACC\uFE00" }, + { "vsupne", "\u228B\uFE00" }, + { "Vvdash", "\u22AA" }, + { "vzigzag", "\u299A" }, + { "Wcirc", "\u0174" }, + { "wcirc", "\u0175" }, + { "wedbar", "\u2A5F" }, + { "Wedge", "\u22C0" }, + { "wedge", "\u2227" }, + { "wedgeq", "\u2259" }, + { "weierp", "\u2118" }, + { "Wfr", "\uD835\uDD1A" }, + { "wfr", "\uD835\uDD34" }, + { "Wopf", "\uD835\uDD4E" }, + { "wopf", "\uD835\uDD68" }, + { "wp", "\u2118" }, + { "wr", "\u2240" }, + { "wreath", "\u2240" }, + { "Wscr", "\uD835\uDCB2" }, + { "wscr", "\uD835\uDCCC" }, + { "xcap", "\u22C2" }, + { "xcirc", "\u25EF" }, + { "xcup", "\u22C3" }, + { "xdtri", "\u25BD" }, + { "Xfr", "\uD835\uDD1B" }, + { "xfr", "\uD835\uDD35" }, + { "xhArr", "\u27FA" }, + { "xharr", "\u27F7" }, + { "Xi", "\u039E" }, + { "xi", "\u03BE" }, + { "xlArr", "\u27F8" }, + { "xlarr", "\u27F5" }, + { "xmap", "\u27FC" }, + { "xnis", "\u22FB" }, + { "xodot", "\u2A00" }, + { "Xopf", "\uD835\uDD4F" }, + { "xopf", "\uD835\uDD69" }, + { "xoplus", "\u2A01" }, + { "xotime", "\u2A02" }, + { "xrArr", "\u27F9" }, + { "xrarr", "\u27F6" }, + { "Xscr", "\uD835\uDCB3" }, + { "xscr", "\uD835\uDCCD" }, + { "xsqcup", "\u2A06" }, + { "xuplus", "\u2A04" }, + { "xutri", "\u25B3" }, + { "xvee", "\u22C1" }, + { "xwedge", "\u22C0" }, + { "Yacute", "\u00DD" }, + { "yacute", "\u00FD" }, + { "YAcy", "\u042F" }, + { "yacy", "\u044F" }, + { "Ycirc", "\u0176" }, + { "ycirc", "\u0177" }, + { "Ycy", "\u042B" }, + { "ycy", "\u044B" }, + { "yen", "\u00A5" }, + { "Yfr", "\uD835\uDD1C" }, + { "yfr", "\uD835\uDD36" }, + { "YIcy", "\u0407" }, + { "yicy", "\u0457" }, + { "Yopf", "\uD835\uDD50" }, + { "yopf", "\uD835\uDD6A" }, + { "Yscr", "\uD835\uDCB4" }, + { "yscr", "\uD835\uDCCE" }, + { "YUcy", "\u042E" }, + { "yucy", "\u044E" }, + { "Yuml", "\u0178" }, + { "yuml", "\u00FF" }, + { "Zacute", "\u0179" }, + { "zacute", "\u017A" }, + { "Zcaron", "\u017D" }, + { "zcaron", "\u017E" }, + { "Zcy", "\u0417" }, + { "zcy", "\u0437" }, + { "Zdot", "\u017B" }, + { "zdot", "\u017C" }, + { "zeetrf", "\u2128" }, + { "ZeroWidthSpace", "\u200B" }, + { "Zeta", "\u0396" }, + { "zeta", "\u03B6" }, + { "Zfr", "\u2128" }, + { "zfr", "\uD835\uDD37" }, + { "ZHcy", "\u0416" }, + { "zhcy", "\u0436" }, + { "zigrarr", "\u21DD" }, + { "Zopf", "\u2124" }, + { "zopf", "\uD835\uDD6B" }, + { "Zscr", "\uD835\uDCB5" }, + { "zscr", "\uD835\uDCCF" }, + { "zwj", "\u200D" }, + { "zwnj", "\u200C" } + }; +#endregion } \ No newline at end of file diff --git a/src/Markdig/Helpers/FastStringWriter.cs b/src/Markdig/Helpers/FastStringWriter.cs index 5172c9ec7..d2e731ebf 100644 --- a/src/Markdig/Helpers/FastStringWriter.cs +++ b/src/Markdig/Helpers/FastStringWriter.cs @@ -2,7 +2,6 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; @@ -11,277 +10,276 @@ using System.Threading; using System.Threading.Tasks; -namespace Markdig.Helpers +namespace Markdig.Helpers; + +internal sealed class FastStringWriter : TextWriter { - internal sealed class FastStringWriter : TextWriter - { - public override Encoding Encoding => Encoding.Unicode; + public override Encoding Encoding => Encoding.Unicode; - private char[] _chars; - private int _pos; - private string _newLine; + private char[] _chars; + private int _pos; + private string _newLine; - public FastStringWriter() - { - _chars = new char[1024]; - _newLine = "\n"; - } + public FastStringWriter() + { + _chars = new char[1024]; + _newLine = "\n"; + } - [AllowNull] - public override string NewLine - { - get => _newLine; - set => _newLine = value ?? Environment.NewLine; - } + [AllowNull] + public override string NewLine + { + get => _newLine; + set => _newLine = value ?? Environment.NewLine; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void Write(char value) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Write(char value) + { + char[] chars = _chars; + int pos = _pos; + if ((uint)pos < (uint)chars.Length) { - char[] chars = _chars; - int pos = _pos; - if ((uint)pos < (uint)chars.Length) - { - chars[pos] = value; - _pos = pos + 1; - } - else - { - GrowAndAppend(value); - } + chars[pos] = value; + _pos = pos + 1; } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void WriteLine(char value) + else { - Write(value); - WriteLine(); + GrowAndAppend(value); } + } - public override Task WriteAsync(char value) - { - Write(value); - return Task.CompletedTask; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void WriteLine(char value) + { + Write(value); + WriteLine(); + } - public override Task WriteLineAsync(char value) - { - WriteLine(value); - return Task.CompletedTask; - } + public override Task WriteAsync(char value) + { + Write(value); + return Task.CompletedTask; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void Write(string? value) + public override Task WriteLineAsync(char value) + { + WriteLine(value); + return Task.CompletedTask; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Write(string? value) + { + if (value is not null) { - if (value is not null) + if (_pos > _chars.Length - value.Length) { - if (_pos > _chars.Length - value.Length) - { - Grow(value.Length); - } - - value.AsSpan().CopyTo(_chars.AsSpan(_pos)); - _pos += value.Length; + Grow(value.Length); } - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void WriteLine(string? value) - { - Write(value); - WriteLine(); + value.AsSpan().CopyTo(_chars.AsSpan(_pos)); + _pos += value.Length; } + } - public override Task WriteAsync(string? value) - { - Write(value); - return Task.CompletedTask; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void WriteLine(string? value) + { + Write(value); + WriteLine(); + } - public override Task WriteLineAsync(string? value) - { - WriteLine(value); - return Task.CompletedTask; - } + public override Task WriteAsync(string? value) + { + Write(value); + return Task.CompletedTask; + } + + public override Task WriteLineAsync(string? value) + { + WriteLine(value); + return Task.CompletedTask; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void Write(char[]? buffer) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Write(char[]? buffer) + { + if (buffer is not null) { - if (buffer is not null) + if (_pos > _chars.Length - buffer.Length) { - if (_pos > _chars.Length - buffer.Length) - { - Grow(buffer.Length); - } - - buffer.CopyTo(_chars.AsSpan(_pos)); - _pos += buffer.Length; + Grow(buffer.Length); } - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void WriteLine(char[]? buffer) - { - Write(buffer); - WriteLine(); + 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) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Write(char[] buffer, int index, int count) + { + if (buffer is not null) { - if (buffer is not null) + if (_pos > _chars.Length - count) { - if (_pos > _chars.Length - count) - { - Grow(buffer.Length); - } - - buffer.AsSpan(index, count).CopyTo(_chars.AsSpan(_pos)); - _pos += count; + Grow(buffer.Length); } - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void WriteLine(char[] buffer, int index, int count) - { - Write(buffer, index, count); - WriteLine(); + buffer.AsSpan(index, count).CopyTo(_chars.AsSpan(_pos)); + _pos += count; } + } - public override Task WriteAsync(char[] buffer, int index, int count) - { - Write(buffer, index, count); - return Task.CompletedTask; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void WriteLine(char[] buffer, int index, int count) + { + Write(buffer, index, count); + WriteLine(); + } - public override Task WriteLineAsync(char[] buffer, int index, int count) - { - WriteLine(buffer, index, count); - return Task.CompletedTask; - } + public override Task WriteAsync(char[] buffer, int index, int count) + { + Write(buffer, index, count); + return Task.CompletedTask; + } + + public override Task WriteLineAsync(char[] buffer, int index, int count) + { + WriteLine(buffer, index, count); + return Task.CompletedTask; + } #if !(NETFRAMEWORK || NETSTANDARD2_0) - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void Write(ReadOnlySpan value) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Write(ReadOnlySpan value) + { + if (_pos > _chars.Length - value.Length) { - if (_pos > _chars.Length - value.Length) - { - Grow(value.Length); - } - - value.CopyTo(_chars.AsSpan(_pos)); - _pos += value.Length; + Grow(value.Length); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void WriteLine(ReadOnlySpan buffer) - { - Write(buffer); - WriteLine(); - } + value.CopyTo(_chars.AsSpan(_pos)); + _pos += value.Length; + } - public override Task WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) - { - Write(buffer.Span); - return Task.CompletedTask; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void WriteLine(ReadOnlySpan buffer) + { + Write(buffer); + WriteLine(); + } - public override Task WriteLineAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) - { - WriteLine(buffer.Span); - return Task.CompletedTask; - } + public override Task WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + Write(buffer.Span); + return Task.CompletedTask; + } + + public override Task WriteLineAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + WriteLine(buffer.Span); + return Task.CompletedTask; + } #endif #if !(NETFRAMEWORK || NETSTANDARD2_0 || NETSTANDARD2_1) - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void Write(StringBuilder? value) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Write(StringBuilder? value) + { + if (value is not null) { - if (value is not null) + int length = value.Length; + if (_pos > _chars.Length - length) { - int length = value.Length; - if (_pos > _chars.Length - length) - { - Grow(length); - } - - value.CopyTo(0, _chars.AsSpan(_pos), length); - _pos += length; + Grow(length); } - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void WriteLine(StringBuilder? value) - { - Write(value); - WriteLine(); + value.CopyTo(0, _chars.AsSpan(_pos), length); + _pos += length; } + } - public override Task WriteAsync(StringBuilder? value, CancellationToken cancellationToken = default) - { - Write(value); - return Task.CompletedTask; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void WriteLine(StringBuilder? value) + { + Write(value); + WriteLine(); + } - public override Task WriteLineAsync(StringBuilder? value, CancellationToken cancellationToken = default) - { - WriteLine(value); - return Task.CompletedTask; - } + public override Task WriteAsync(StringBuilder? value, CancellationToken cancellationToken = default) + { + Write(value); + return Task.CompletedTask; + } + + public override Task WriteLineAsync(StringBuilder? value, CancellationToken cancellationToken = default) + { + WriteLine(value); + return Task.CompletedTask; + } #endif - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void WriteLine() + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void WriteLine() + { + foreach (char c in _newLine) { - foreach (char c in _newLine) - { - Write(c); - } + Write(c); } + } - public override Task WriteLineAsync() - { - WriteLine(); - return Task.CompletedTask; - } + public override Task WriteLineAsync() + { + WriteLine(); + return Task.CompletedTask; + } - [MethodImpl(MethodImplOptions.NoInlining)] - private void GrowAndAppend(char value) - { - Grow(1); - Write(value); - } + [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."); + 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; - } + 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 Flush() { } - public override void Close() { } + public override void Close() { } - public override Task FlushAsync() => Task.CompletedTask; + public override Task FlushAsync() => Task.CompletedTask; #if !(NETFRAMEWORK || NETSTANDARD2_0) - public override ValueTask DisposeAsync() => default; + public override ValueTask DisposeAsync() => default; #endif - public void Reset() - { - _pos = 0; - } + public void Reset() + { + _pos = 0; + } - public override string ToString() - { - return _chars.AsSpan(0, _pos).ToString(); - } + public override string ToString() + { + return _chars.AsSpan(0, _pos).ToString(); } } diff --git a/src/Markdig/Helpers/HtmlHelper.cs b/src/Markdig/Helpers/HtmlHelper.cs index d9a51fd76..90cecf3ce 100644 --- a/src/Markdig/Helpers/HtmlHelper.cs +++ b/src/Markdig/Helpers/HtmlHelper.cs @@ -2,643 +2,641 @@ // 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.CodeAnalysis; using System.Runtime.CompilerServices; -namespace Markdig.Helpers +namespace Markdig.Helpers; + +/// +/// Helper to parse several HTML tags. +/// +public static class HtmlHelper { - /// - /// Helper to parse several HTML tags. - /// - public static class HtmlHelper - { - private static readonly char[] SearchBackAndAmp = { '\\', '&' }; - private static readonly char[] SearchAmp = { '&' }; - private static readonly string[] EscapeUrlsForAscii = new string[128]; + private static readonly char[] SearchBackAndAmp = { '\\', '&' }; + private static readonly char[] SearchAmp = { '&' }; + private static readonly string[] EscapeUrlsForAscii = new string[128]; - static HtmlHelper() + static HtmlHelper() + { + for (int i = 0; i < EscapeUrlsForAscii.Length; i++) { - for (int i = 0; i < EscapeUrlsForAscii.Length; i++) + if (i <= 32 || @"""'<>[\]^`{|}~".IndexOf((char)i) >= 0 || i == 127) + { + EscapeUrlsForAscii[i] = $"%{i:X2}"; + } + else if ((char) i == '&') { - if (i <= 32 || @"""'<>[\]^`{|}~".IndexOf((char)i) >= 0 || i == 127) + EscapeUrlsForAscii[i] = "&"; + } + } + } + + public static string? EscapeUrlCharacter(char c) + { + return c < 128 ? EscapeUrlsForAscii[c] : null; + } + + public static bool TryParseHtmlTag(ref StringSlice text, [NotNullWhen(true)] out string? htmlTag) + { + var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); + if (TryParseHtmlTag(ref text, ref builder)) + { + htmlTag = builder.ToString(); + return true; + } + else + { + builder.Dispose(); + htmlTag = null; + return false; + } + } + + private static bool TryParseHtmlTag(ref StringSlice text, ref ValueStringBuilder builder) + { + var c = text.CurrentChar; + if (c != '<') + { + return false; + } + c = text.NextChar(); + + builder.Append('<'); + + switch (c) + { + case '/': + return TryParseHtmlCloseTag(ref text, ref builder); + case '?': + return TryParseHtmlTagProcessingInstruction(ref text, ref builder); + case '!': + builder.Append(c); + c = text.NextChar(); + if (c == '-') { - EscapeUrlsForAscii[i] = $"%{i:X2}"; + return TryParseHtmlTagHtmlComment(ref text, ref builder); } - else if ((char) i == '&') + + if (c == '[') { - EscapeUrlsForAscii[i] = "&"; + return TryParseHtmlTagCData(ref text, ref builder); } - } + + return TryParseHtmlTagDeclaration(ref text, ref builder); } - public static string? EscapeUrlCharacter(char c) + return TryParseHtmlTagOpenTag(ref text, ref builder); + } + + internal static bool TryParseHtmlTagOpenTag(ref StringSlice text, ref ValueStringBuilder builder) + { + var c = text.CurrentChar; + + // Parse the tagname + if (!c.IsAlpha()) { - return c < 128 ? EscapeUrlsForAscii[c] : null; + return false; } + builder.Append(c); - public static bool TryParseHtmlTag(ref StringSlice text, [NotNullWhen(true)] out string? htmlTag) + while (true) { - var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); - if (TryParseHtmlTag(ref text, ref builder)) + c = text.NextChar(); + if (c.IsAlphaNumeric() || c == '-') { - htmlTag = builder.ToString(); - return true; + builder.Append(c); } else { - builder.Dispose(); - htmlTag = null; - return false; + break; } } - private static bool TryParseHtmlTag(ref StringSlice text, ref ValueStringBuilder builder) + bool hasAttribute = false; + while (true) { - var c = text.CurrentChar; - if (c != '<') + var hasWhitespaces = false; + // Skip any whitespaces + while (c.IsWhitespace()) { - return false; + builder.Append(c); + c = text.NextChar(); + hasWhitespaces = true; } - c = text.NextChar(); - - builder.Append('<'); switch (c) { - case '/': - return TryParseHtmlCloseTag(ref text, ref builder); - case '?': - return TryParseHtmlTagProcessingInstruction(ref text, ref builder); - case '!': + case '\0': + return false; + case '>': + text.SkipChar(); builder.Append(c); + return true; + case '/': + builder.Append('/'); c = text.NextChar(); - if (c == '-') + if (c != '>') { - return TryParseHtmlTagHtmlComment(ref text, ref builder); + return false; } + text.SkipChar(); + builder.Append('>'); + return true; + case '=': - if (c == '[') + if (!hasAttribute) { - return TryParseHtmlTagCData(ref text, ref builder); + return false; } - return TryParseHtmlTagDeclaration(ref text, ref builder); - } - - return TryParseHtmlTagOpenTag(ref text, ref builder); - } - - internal static bool TryParseHtmlTagOpenTag(ref StringSlice text, ref ValueStringBuilder builder) - { - var c = text.CurrentChar; - - // Parse the tagname - if (!c.IsAlpha()) - { - return false; - } - builder.Append(c); - - while (true) - { - c = text.NextChar(); - if (c.IsAlphaNumeric() || c == '-') - { - builder.Append(c); - } - else - { - break; - } - } + builder.Append('='); - bool hasAttribute = false; - while (true) - { - var hasWhitespaces = false; - // Skip any whitespaces - while (c.IsWhitespace()) - { - builder.Append(c); + // Skip any spaces after c = text.NextChar(); - hasWhitespaces = true; - } - - switch (c) - { - case '\0': - return false; - case '>': - text.SkipChar(); + while (c.IsWhitespace()) + { builder.Append(c); - return true; - case '/': - builder.Append('/'); c = text.NextChar(); - if (c != '>') - { - return false; - } - text.SkipChar(); - builder.Append('>'); - return true; - case '=': - - if (!hasAttribute) - { - return false; - } - - builder.Append('='); + } - // Skip any spaces after - c = text.NextChar(); - while (c.IsWhitespace()) + // Parse a quoted string + if (c == '\'' || c == '\"') + { + builder.Append(c); + char openingStringChar = c; + while (true) { - builder.Append(c); c = text.NextChar(); + if (c == '\0') + { + return false; + } + if (c != openingStringChar) + { + builder.Append(c); + } + else + { + break; + } } - - // Parse a quoted string - if (c == '\'' || c == '\"') + builder.Append(c); + c = text.NextChar(); + } + else + { + // Parse until we match a space or a special html character + int matchCount = 0; + while (true) { - builder.Append(c); - char openingStringChar = c; - while (true) + if (c == '\0') { - c = text.NextChar(); - if (c == '\0') - { - return false; - } - if (c != openingStringChar) - { - builder.Append(c); - } - else - { - break; - } + return false; + } + if (IsSpaceOrSpecialHtmlChar(c)) + { + break; } + matchCount++; builder.Append(c); c = text.NextChar(); } - else + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static bool IsSpaceOrSpecialHtmlChar(char c) { - // Parse until we match a space or a special html character - int matchCount = 0; - while (true) + if (c > '>') { - if (c == '\0') - { - return false; - } - if (IsSpaceOrSpecialHtmlChar(c)) - { - break; - } - matchCount++; - builder.Append(c); - c = text.NextChar(); + return c == '`'; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static bool IsSpaceOrSpecialHtmlChar(char c) - { - if (c > '>') - { - return c == '`'; - } - - const long BitMask = - (1L << ' ') - | (1L << '\n') - | (1L << '"') - | (1L << '\'') - | (1L << '=') - | (1L << '<') - | (1L << '>'); - - return (BitMask & (1L << c)) != 0; - } + const long BitMask = + (1L << ' ') + | (1L << '\n') + | (1L << '"') + | (1L << '\'') + | (1L << '=') + | (1L << '<') + | (1L << '>'); - // We need at least one char after '=' - if (matchCount == 0) - { - return false; - } + return (BitMask & (1L << c)) != 0; } - hasAttribute = false; - continue; - default: - if (!hasWhitespaces) + // We need at least one char after '=' + if (matchCount == 0) { return false; } + } + + hasAttribute = false; + continue; + default: + if (!hasWhitespaces) + { + return false; + } - // Parse the attribute name - if (!(c.IsAlpha() || c == '_' || c == ':')) + // Parse the attribute name + if (!(c.IsAlpha() || c == '_' || c == ':')) + { + return false; + } + builder.Append(c); + + while (true) + { + c = text.NextChar(); + if (c.IsAlphaNumeric() || IsCharToAppend(c)) { - return false; + builder.Append(c); + } + else + { + break; } - builder.Append(c); - while (true) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static bool IsCharToAppend(char c) { - c = text.NextChar(); - if (c.IsAlphaNumeric() || IsCharToAppend(c)) - { - builder.Append(c); - } - else + if ((uint)(c - '-') > '_' - '-') { - break; + return false; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static bool IsCharToAppend(char c) - { - if ((uint)(c - '-') > '_' - '-') - { - return false; - } - - const long BitMask = - (1L << '_') - | (1L << ':') - | (1L << '.') - | (1L << '-'); - - return (BitMask & (1L << c)) != 0; - } + const long BitMask = + (1L << '_') + | (1L << ':') + | (1L << '.') + | (1L << '-'); + + return (BitMask & (1L << c)) != 0; } + } - hasAttribute = true; - break; - } + hasAttribute = true; + break; } } + } + + private static bool TryParseHtmlTagDeclaration(ref StringSlice text, ref ValueStringBuilder builder) + { + var c = text.CurrentChar; + bool hasAlpha = false; + while (c.IsAlphaUpper()) + { + builder.Append(c); + c = text.NextChar(); + hasAlpha = true; + } + + if (!hasAlpha || !c.IsWhitespace()) + { + return false; + } - private static bool TryParseHtmlTagDeclaration(ref StringSlice text, ref ValueStringBuilder builder) + // Regexp: "\\![A-Z]+\\s+[^>\\x00]*>" + while (true) { - var c = text.CurrentChar; - bool hasAlpha = false; - while (c.IsAlphaUpper()) + builder.Append(c); + c = text.NextChar(); + if (c == '\0') { - builder.Append(c); - c = text.NextChar(); - hasAlpha = true; + return false; } - if (!hasAlpha || !c.IsWhitespace()) + if (c == '>') { - return false; + text.SkipChar(); + builder.Append('>'); + return true; } + } + } + + private static bool TryParseHtmlTagCData(ref StringSlice text, ref ValueStringBuilder builder) + { + if (text.Match("[CDATA[")) + { + builder.Append("[CDATA["); + text.Start += 6; - // Regexp: "\\![A-Z]+\\s+[^>\\x00]*>" + char c = '\0'; while (true) { - builder.Append(c); + var pc = c; c = text.NextChar(); if (c == '\0') { return false; } - if (c == '>') + builder.Append(c); + + if (c == ']' && pc == ']' && text.PeekChar() == '>') { + text.SkipChar(); text.SkipChar(); builder.Append('>'); return true; } } } + return false; + } - private static bool TryParseHtmlTagCData(ref StringSlice text, ref ValueStringBuilder builder) + internal static bool TryParseHtmlCloseTag(ref StringSlice text, ref ValueStringBuilder builder) + { + // + builder.Append('/'); + + var c = text.NextChar(); + if (!c.IsAlpha()) { - if (text.Match("[CDATA[")) + return false; + } + builder.Append(c); + + bool skipSpaces = false; + while (true) + { + c = text.NextChar(); + if (c == '>') { - builder.Append("[CDATA["); - text.Start += 6; + text.SkipChar(); + builder.Append('>'); + return true; + } - char c = '\0'; - while (true) + if (skipSpaces) + { + if (c != ' ') { - var pc = c; - c = text.NextChar(); - if (c == '\0') - { - return false; - } - - builder.Append(c); - - if (c == ']' && pc == ']' && text.PeekChar() == '>') - { - text.SkipChar(); - text.SkipChar(); - builder.Append('>'); - return true; - } + break; } } - return false; + else if (c == ' ') + { + skipSpaces = true; + } + else if (!(c.IsAlphaNumeric() || c == '-')) + { + break; + } + + builder.Append(c); } + return false; + } - internal static bool TryParseHtmlCloseTag(ref StringSlice text, ref ValueStringBuilder builder) + + private static bool TryParseHtmlTagHtmlComment(ref StringSlice text, ref ValueStringBuilder builder) + { + var c = text.NextChar(); + if (c != '-') + { + return false; + } + builder.Append('-'); + builder.Append('-'); + if (text.PeekChar() == '>') { - // - builder.Append('/'); + return false; + } - var c = text.NextChar(); - if (!c.IsAlpha()) + var countHyphen = 0; + while (true) + { + c = text.NextChar(); + if (c == '\0') { return false; } - builder.Append(c); - bool skipSpaces = false; - while (true) + if (countHyphen == 2) { - c = text.NextChar(); if (c == '>') { - text.SkipChar(); builder.Append('>'); + text.SkipChar(); return true; } - - if (skipSpaces) - { - if (c != ' ') - { - break; - } - } - else if (c == ' ') - { - skipSpaces = true; - } - else if (!(c.IsAlphaNumeric() || c == '-')) - { - break; - } - - builder.Append(c); + return false; } - return false; + countHyphen = c == '-' ? countHyphen + 1 : 0; + builder.Append(c); } + } - - private static bool TryParseHtmlTagHtmlComment(ref StringSlice text, ref ValueStringBuilder builder) + private static bool TryParseHtmlTagProcessingInstruction(ref StringSlice text, ref ValueStringBuilder builder) + { + builder.Append('?'); + var prevChar = '\0'; + while (true) { var c = text.NextChar(); - if (c != '-') - { - return false; - } - builder.Append('-'); - builder.Append('-'); - if (text.PeekChar() == '>') + if (c == '\0') { return false; } - var countHyphen = 0; - while (true) + if (c == '>' && prevChar == '?') { - c = text.NextChar(); - if (c == '\0') - { - return false; - } - - if (countHyphen == 2) - { - if (c == '>') - { - builder.Append('>'); - text.SkipChar(); - return true; - } - return false; - } - countHyphen = c == '-' ? countHyphen + 1 : 0; - builder.Append(c); + builder.Append('>'); + text.SkipChar(); + return true; } + prevChar = c; + builder.Append(c); } + } - private static bool TryParseHtmlTagProcessingInstruction(ref StringSlice text, ref ValueStringBuilder builder) + /// + /// Destructively unescape a string: remove backslashes before punctuation or symbol characters. + /// + /// The string data that will be changed by unescaping any punctuation or symbol characters. + /// if set to true [remove back slash]. + /// + public static string Unescape(string? text, bool removeBackSlash = true) + { + // Credits: code from CommonMark.NET + // Copyright (c) 2014, Kārlis Gaņģis All rights reserved. + // See license for details: https://github.com/Knagis/CommonMark.NET/blob/master/LICENSE.md + if (string.IsNullOrEmpty(text)) { - builder.Append('?'); - var prevChar = '\0'; - while (true) - { - var c = text.NextChar(); - if (c == '\0') - { - return false; - } - - if (c == '>' && prevChar == '?') - { - builder.Append('>'); - text.SkipChar(); - return true; - } - prevChar = c; - builder.Append(c); - } + return string.Empty; } - /// - /// Destructively unescape a string: remove backslashes before punctuation or symbol characters. - /// - /// The string data that will be changed by unescaping any punctuation or symbol characters. - /// if set to true [remove back slash]. - /// - public static string Unescape(string? text, bool removeBackSlash = true) + // remove backslashes before punctuation chars: + int searchPos = 0; + int lastPos = 0; + char c = '\0'; + char[] search = removeBackSlash ? SearchBackAndAmp : SearchAmp; + var sb = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); + + while ((searchPos = text!.IndexOfAny(search, searchPos)) != -1) { - // Credits: code from CommonMark.NET - // Copyright (c) 2014, Kārlis Gaņģis All rights reserved. - // See license for details: https://github.com/Knagis/CommonMark.NET/blob/master/LICENSE.md - if (string.IsNullOrEmpty(text)) + c = text[searchPos]; + if (removeBackSlash && c == '\\') { - return string.Empty; - } + searchPos++; - // remove backslashes before punctuation chars: - int searchPos = 0; - int lastPos = 0; - char c = '\0'; - char[] search = removeBackSlash ? SearchBackAndAmp : SearchAmp; - var sb = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); + if (text.Length == searchPos) + break; - while ((searchPos = text!.IndexOfAny(search, searchPos)) != -1) - { c = text[searchPos]; - if (removeBackSlash && c == '\\') + if (c.IsEscapableSymbol()) + { + sb.Append(text.AsSpan(lastPos, searchPos - lastPos - 1)); + lastPos = searchPos; + } + } + else if (c == '&') + { + var match = ScanEntity(new StringSlice(text, searchPos, text.Length - 1), out int numericEntity, out int entityNameStart, out int entityNameLength); + if (match == 0) { searchPos++; - - if (text.Length == searchPos) - break; - - c = text[searchPos]; - if (c.IsEscapableSymbol()) - { - sb.Append(text.AsSpan(lastPos, searchPos - lastPos - 1)); - lastPos = searchPos; - } } - else if (c == '&') + else { - var match = ScanEntity(new StringSlice(text, searchPos, text.Length - 1), out int numericEntity, out int entityNameStart, out int entityNameLength); - if (match == 0) - { - searchPos++; - } - else - { - searchPos += match; + searchPos += match; - if (entityNameLength > 0) - { - var decoded = EntityHelper.DecodeEntity(text.AsSpan(entityNameStart, entityNameLength)); - if (decoded != null) - { - sb.Append(text.AsSpan(lastPos, searchPos - match - lastPos)); - sb.Append(decoded); - lastPos = searchPos; - } - } - else if (numericEntity >= 0) + if (entityNameLength > 0) + { + var decoded = EntityHelper.DecodeEntity(text.AsSpan(entityNameStart, entityNameLength)); + if (decoded != null) { sb.Append(text.AsSpan(lastPos, searchPos - match - lastPos)); - EntityHelper.DecodeEntity(numericEntity, ref sb); + sb.Append(decoded); lastPos = searchPos; } } + else if (numericEntity >= 0) + { + sb.Append(text.AsSpan(lastPos, searchPos - match - lastPos)); + EntityHelper.DecodeEntity(numericEntity, ref sb); + lastPos = searchPos; + } } } - - if (c == 0) - { - sb.Dispose(); - return text; - } - - sb.Append(text.AsSpan(lastPos, text.Length - lastPos)); - return sb.ToString(); } - /// - /// Scans an entity. - /// Returns number of chars matched. - /// - public static int ScanEntity(T slice, out int numericEntity, out int namedEntityStart, out int namedEntityLength) where T : ICharIterator + if (c == 0) { - // Credits: code from CommonMark.NET - // Copyright (c) 2014, Kārlis Gaņģis All rights reserved. - // See license for details: https://github.com/Knagis/CommonMark.NET/blob/master/LICENSE.md + sb.Dispose(); + return text; + } - numericEntity = 0; - namedEntityStart = 0; - namedEntityLength = 0; + sb.Append(text.AsSpan(lastPos, text.Length - lastPos)); + return sb.ToString(); + } - if (slice.CurrentChar != '&' || slice.PeekChar(3) == '\0') - { - return 0; - } + /// + /// Scans an entity. + /// Returns number of chars matched. + /// + public static int ScanEntity(T slice, out int numericEntity, out int namedEntityStart, out int namedEntityLength) where T : ICharIterator + { + // Credits: code from CommonMark.NET + // Copyright (c) 2014, Kārlis Gaņģis All rights reserved. + // See license for details: https://github.com/Knagis/CommonMark.NET/blob/master/LICENSE.md - var start = slice.Start; - char c = slice.NextChar(); - int counter = 0; - - if (c == '#') - { - c = slice.PeekChar(); - if ((c | 0x20) == 'x') - { - c = slice.NextChar(); // skip # - // expect 1-6 hex digits starting from pos+3 - while (c != '\0') - { - c = slice.NextChar(); + numericEntity = 0; + namedEntityStart = 0; + namedEntityLength = 0; - if (c.IsDigit()) - { - if (++counter == 7) return 0; - numericEntity = numericEntity * 16 + (c - '0'); - continue; - } - else if ((uint)((c - 'A') & ~0x20) <= ('F' - 'A')) - { - if (++counter == 7) return 0; - numericEntity = numericEntity * 16 + ((c | 0x20) - 'a' + 10); - continue; - } + if (slice.CurrentChar != '&' || slice.PeekChar(3) == '\0') + { + return 0; + } - if (c == ';') - return counter == 0 ? 0 : slice.Start - start + 1; + var start = slice.Start; + char c = slice.NextChar(); + int counter = 0; + + if (c == '#') + { + c = slice.PeekChar(); + if ((c | 0x20) == 'x') + { + c = slice.NextChar(); // skip # + // expect 1-6 hex digits starting from pos+3 + while (c != '\0') + { + c = slice.NextChar(); - return 0; + if (c.IsDigit()) + { + if (++counter == 7) return 0; + numericEntity = numericEntity * 16 + (c - '0'); + continue; } - } - else - { - // expect 1-7 digits starting from pos+2 - while (c != '\0') + else if ((uint)((c - 'A') & ~0x20) <= ('F' - 'A')) { - c = slice.NextChar(); - - if (c.IsDigit()) - { - if (++counter == 8) return 0; - numericEntity = numericEntity * 10 + (c - '0'); - continue; - } + if (++counter == 7) return 0; + numericEntity = numericEntity * 16 + ((c | 0x20) - 'a' + 10); + continue; + } - if (c == ';') - return counter == 0 ? 0 : slice.Start - start + 1; + if (c == ';') + return counter == 0 ? 0 : slice.Start - start + 1; - return 0; - } + return 0; } } else { - // expect a letter and 1-31 letters or digits - if (!c.IsAlpha()) - return 0; - - namedEntityStart = slice.Start; - namedEntityLength++; - + // expect 1-7 digits starting from pos+2 while (c != '\0') { c = slice.NextChar(); - if (c.IsAlphaNumeric()) + if (c.IsDigit()) { - if (++counter == 32) - return 0; - namedEntityLength++; + if (++counter == 8) return 0; + numericEntity = numericEntity * 10 + (c - '0'); continue; } if (c == ';') - { return counter == 0 ? 0 : slice.Start - start + 1; - } return 0; } } + } + else + { + // expect a letter and 1-31 letters or digits + if (!c.IsAlpha()) + return 0; - return 0; + namedEntityStart = slice.Start; + namedEntityLength++; + + while (c != '\0') + { + c = slice.NextChar(); + + if (c.IsAlphaNumeric()) + { + if (++counter == 32) + return 0; + namedEntityLength++; + continue; + } + + if (c == ';') + { + return counter == 0 ? 0 : slice.Start - start + 1; + } + + return 0; + } } + + return 0; } } \ No newline at end of file diff --git a/src/Markdig/Helpers/ICharIterator.cs b/src/Markdig/Helpers/ICharIterator.cs index cb11ad1a5..99ee0bbb4 100644 --- a/src/Markdig/Helpers/ICharIterator.cs +++ b/src/Markdig/Helpers/ICharIterator.cs @@ -2,62 +2,61 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -namespace Markdig.Helpers +namespace Markdig.Helpers; + +/// +/// Provides a common interface for iterating characters +/// over a or . +/// +public interface ICharIterator { /// - /// Provides a common interface for iterating characters - /// over a or . - /// - public interface ICharIterator - { - /// - /// Gets the current start character position. - /// - int Start { get; } - - /// - /// Gets the current character. - /// - char CurrentChar { get; } - - /// - /// Gets the end character position. - /// - int End { get; } - - /// - /// Goes to the next character, incrementing the position. - /// - /// The next character. `\0` is end of the iteration. - char NextChar(); - - /// - /// Goes to the next character, incrementing the position. - /// - void SkipChar(); - - /// - /// Peeks at the next character, without incrementing the position. - /// - /// The next character. `\0` is end of the iteration. - char PeekChar(); - - /// - /// Peeks at the next character, without incrementing the position. - /// - /// - /// The next character. `\0` is end of the iteration. - char PeekChar(int offset); - - /// - /// Gets a value indicating whether this instance is empty. - /// - bool IsEmpty { get; } - - /// - /// Trims whitespaces at the beginning of this slice starting from position. - /// - /// true if it has reaches the end of the iterator - bool TrimStart(); - } + /// Gets the current start character position. + /// + int Start { get; } + + /// + /// Gets the current character. + /// + char CurrentChar { get; } + + /// + /// Gets the end character position. + /// + int End { get; } + + /// + /// Goes to the next character, incrementing the position. + /// + /// The next character. `\0` is end of the iteration. + char NextChar(); + + /// + /// Goes to the next character, incrementing the position. + /// + void SkipChar(); + + /// + /// Peeks at the next character, without incrementing the position. + /// + /// The next character. `\0` is end of the iteration. + char PeekChar(); + + /// + /// Peeks at the next character, without incrementing the position. + /// + /// + /// The next character. `\0` is end of the iteration. + char PeekChar(int offset); + + /// + /// Gets a value indicating whether this instance is empty. + /// + bool IsEmpty { get; } + + /// + /// Trims whitespaces at the beginning of this slice starting from position. + /// + /// true if it has reaches the end of the iterator + bool TrimStart(); } \ No newline at end of file diff --git a/src/Markdig/Helpers/LazySubstring.cs b/src/Markdig/Helpers/LazySubstring.cs index 8f3cf2dbb..f6d8a1b9b 100644 --- a/src/Markdig/Helpers/LazySubstring.cs +++ b/src/Markdig/Helpers/LazySubstring.cs @@ -2,43 +2,41 @@ // 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; -namespace Markdig.Helpers +namespace Markdig.Helpers; + +internal struct LazySubstring { - internal struct LazySubstring - { - private string _text; - public int Offset; - public int Length; + private string _text; + public int Offset; + public int Length; - public LazySubstring(string text) - { - _text = text; - Offset = 0; - Length = text.Length; - } + public LazySubstring(string text) + { + _text = text; + Offset = 0; + Length = text.Length; + } - public LazySubstring(string text, int offset, int length) - { - Debug.Assert((ulong)offset + (ulong)length <= (ulong)text.Length, $"{offset}-{length} in {text}"); - _text = text; - Offset = offset; - Length = length; - } + public LazySubstring(string text, int offset, int length) + { + Debug.Assert((ulong)offset + (ulong)length <= (ulong)text.Length, $"{offset}-{length} in {text}"); + _text = text; + Offset = offset; + Length = length; + } - public ReadOnlySpan AsSpan() => _text.AsSpan(Offset, Length); + public ReadOnlySpan AsSpan() => _text.AsSpan(Offset, Length); - public override string ToString() + public override string ToString() + { + if (Offset != 0 || Length != _text.Length) { - if (Offset != 0 || Length != _text.Length) - { - _text = _text.Substring(Offset, Length); - Offset = 0; - } - - return _text; + _text = _text.Substring(Offset, Length); + Offset = 0; } + + return _text; } } diff --git a/src/Markdig/Helpers/LineReader.cs b/src/Markdig/Helpers/LineReader.cs index 59033e50c..0b2938384 100644 --- a/src/Markdig/Helpers/LineReader.cs +++ b/src/Markdig/Helpers/LineReader.cs @@ -2,94 +2,92 @@ // 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.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -namespace Markdig.Helpers +namespace Markdig.Helpers; + +/// +/// A line reader from a that can provide precise source position +/// +public struct LineReader { + private readonly string _text; + /// - /// A line reader from a that can provide precise source position + /// Initializes a new instance of the class. /// - public struct LineReader + /// + /// bufferSize cannot be <= 0 + public LineReader(string text) { - private readonly string _text; + if (text is null) + ThrowHelper.ArgumentNullException_text(); - /// - /// Initializes a new instance of the class. - /// - /// - /// bufferSize cannot be <= 0 - public LineReader(string text) - { - if (text is null) - ThrowHelper.ArgumentNullException_text(); + _text = text; + SourcePosition = 0; + } - _text = text; - SourcePosition = 0; - } + /// + /// Gets the char position of the line. Valid for the next line before calling . + /// + public int SourcePosition { get; private set; } - /// - /// Gets the char position of the line. Valid for the next line before calling . - /// - public int SourcePosition { get; private set; } + /// + /// Reads a new line from the underlying and update the for the next line. + /// + /// A new line or null if the end of has been reached + public StringSlice ReadLine() + { + string? text = _text; + int end = text.Length; + int sourcePosition = SourcePosition; + int newSourcePosition = int.MaxValue; + NewLine newLine = NewLine.None; - /// - /// Reads a new line from the underlying and update the for the next line. - /// - /// A new line or null if the end of has been reached - public StringSlice ReadLine() + if ((uint)sourcePosition >= (uint)end) + { + text = null; + } + else { - string? text = _text; - int end = text.Length; - int sourcePosition = SourcePosition; - int newSourcePosition = int.MaxValue; - NewLine newLine = NewLine.None; - - if ((uint)sourcePosition >= (uint)end) - { - text = null; - } - else - { #if NETCOREAPP3_1_OR_GREATER - ReadOnlySpan span = MemoryMarshal.CreateReadOnlySpan(ref Unsafe.Add(ref Unsafe.AsRef(text.GetPinnableReference()), sourcePosition), end - sourcePosition); + ReadOnlySpan span = MemoryMarshal.CreateReadOnlySpan(ref Unsafe.Add(ref Unsafe.AsRef(text.GetPinnableReference()), sourcePosition), end - sourcePosition); #else - ReadOnlySpan span = text.AsSpan(sourcePosition); + ReadOnlySpan span = text.AsSpan(sourcePosition); #endif - int crlf = span.IndexOfAny('\r', '\n'); - if (crlf >= 0) - { - end = sourcePosition + crlf; - newSourcePosition = end + 1; + int crlf = span.IndexOfAny('\r', '\n'); + if (crlf >= 0) + { + end = sourcePosition + crlf; + newSourcePosition = end + 1; #if NETCOREAPP3_1_OR_GREATER - if (Unsafe.Add(ref Unsafe.AsRef(text.GetPinnableReference()), end) == '\r') + if (Unsafe.Add(ref Unsafe.AsRef(text.GetPinnableReference()), end) == '\r') #else - if ((uint)end < (uint)text.Length && text[end] == '\r') + if ((uint)end < (uint)text.Length && text[end] == '\r') #endif + { + if ((uint)(newSourcePosition) < (uint)text.Length && text[newSourcePosition] == '\n') { - if ((uint)(newSourcePosition) < (uint)text.Length && text[newSourcePosition] == '\n') - { - newLine = NewLine.CarriageReturnLineFeed; - newSourcePosition++; - } - else - { - newLine = NewLine.CarriageReturn; - } + newLine = NewLine.CarriageReturnLineFeed; + newSourcePosition++; } else { - newLine = NewLine.LineFeed; + newLine = NewLine.CarriageReturn; } } + else + { + newLine = NewLine.LineFeed; + } } - - SourcePosition = newSourcePosition; - return new StringSlice(text, sourcePosition, end - 1, newLine, dummy: false); } + + SourcePosition = newSourcePosition; + return new StringSlice(text, sourcePosition, end - 1, newLine, dummy: false); } } \ No newline at end of file diff --git a/src/Markdig/Helpers/LinkHelper.cs b/src/Markdig/Helpers/LinkHelper.cs index e7a3593d9..52c0c027c 100644 --- a/src/Markdig/Helpers/LinkHelper.cs +++ b/src/Markdig/Helpers/LinkHelper.cs @@ -6,1584 +6,1583 @@ using System.Runtime.CompilerServices; using Markdig.Syntax; -namespace Markdig.Helpers +namespace Markdig.Helpers; + +/// +/// Helpers to parse Markdown links. +/// +public static class LinkHelper { - /// - /// Helpers to parse Markdown links. - /// - public static class LinkHelper + public static bool TryParseAutolink(StringSlice text, [NotNullWhen(true)] out string? link, out bool isEmail) { - public static bool TryParseAutolink(StringSlice text, [NotNullWhen(true)] out string? link, out bool isEmail) - { - return TryParseAutolink(ref text, out link, out isEmail); - } + return TryParseAutolink(ref text, out link, out isEmail); + } - public static string Urilize(string headingText, bool allowOnlyAscii, bool keepOpeningDigits = false) + public static string Urilize(string headingText, bool allowOnlyAscii, bool keepOpeningDigits = false) + { + 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++) { - 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++) + var c = headingText[i]; + var normalized = allowOnlyAscii ? CharNormalizer.ConvertToAscii(c) : null; + for (int j = 0; j < (normalized?.Length ?? 1); j++) { - var c = headingText[i]; - var normalized = allowOnlyAscii ? CharNormalizer.ConvertToAscii(c) : null; - for (int j = 0; j < (normalized?.Length ?? 1); j++) + if (normalized != null) { - if (normalized != null) - { - c = normalized[j]; - } + c = normalized[j]; + } - if (char.IsLetter(c)) + if (char.IsLetter(c)) + { + if (allowOnlyAscii && (c < ' ' || c >= 127)) { - if (allowOnlyAscii && (c < ' ' || c >= 127)) - { - continue; - } - c = char.IsUpper(c) ? char.ToLowerInvariant(c) : c; - headingBuffer.Append(c); - hasLetter = true; - previousIsSpace = false; + continue; } - else if (hasLetter) + c = char.IsUpper(c) ? char.ToLowerInvariant(c) : c; + headingBuffer.Append(c); + hasLetter = true; + previousIsSpace = false; + } + else if (hasLetter) + { + if (IsReservedPunctuation(c)) { - if (IsReservedPunctuation(c)) + if (previousIsSpace) { - if (previousIsSpace) - { - headingBuffer.Length--; - } - if (headingBuffer[headingBuffer.Length - 1] != c) - { - headingBuffer.Append(c); - } - previousIsSpace = false; + headingBuffer.Length--; } - else if (c.IsDigit()) + if (headingBuffer[headingBuffer.Length - 1] != c) { headingBuffer.Append(c); - previousIsSpace = false; } - else if (!previousIsSpace && c.IsWhitespace()) + previousIsSpace = false; + } + else if (c.IsDigit()) + { + headingBuffer.Append(c); + previousIsSpace = false; + } + else if (!previousIsSpace && c.IsWhitespace()) + { + var pc = headingBuffer[headingBuffer.Length - 1]; + if (!IsReservedPunctuation(pc)) { - var pc = headingBuffer[headingBuffer.Length - 1]; - if (!IsReservedPunctuation(pc)) - { - headingBuffer.Append('-'); - } - previousIsSpace = true; + headingBuffer.Append('-'); } + previousIsSpace = true; } } } + } - // Trim trailing _ - . - while (headingBuffer.Length > 0) + // Trim trailing _ - . + while (headingBuffer.Length > 0) + { + var c = headingBuffer[headingBuffer.Length - 1]; + if (IsReservedPunctuation(c)) { - var c = headingBuffer[headingBuffer.Length - 1]; - if (IsReservedPunctuation(c)) - { - headingBuffer.Length--; - } - else - { - break; - } + headingBuffer.Length--; + } + else + { + break; } - - return headingBuffer.ToString(); } - public static string UrilizeAsGfm(string headingText) + 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 = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); + for (int i = 0; i < headingText.Length; i++) { - // Following https://github.com/jch/html-pipeline/blob/master/lib/html/pipeline/toc_filter.rb - var headingBuffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); - for (int i = 0; i < headingText.Length; i++) + var c = headingText[i]; + if (char.IsLetterOrDigit(c) || c == ' ' || c == '-' || c == '_') { - var c = headingText[i]; - if (char.IsLetterOrDigit(c) || c == ' ' || c == '-' || c == '_') - { - headingBuffer.Append(c == ' ' ? '-' : char.ToLowerInvariant(c)); - } + headingBuffer.Append(c == ' ' ? '-' : char.ToLowerInvariant(c)); } - return headingBuffer.ToString(); } + return headingBuffer.ToString(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsReservedPunctuation(char c) + { + return c == '_' || c == '-' || c == '.'; + } + + public static bool TryParseAutolink(ref StringSlice text, [NotNullWhen(true)] out string? link, out bool isEmail) + { + link = null; + isEmail = false; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsReservedPunctuation(char c) + var c = text.CurrentChar; + if (c != '<') { - return c == '_' || c == '-' || c == '.'; + return false; } - public static bool TryParseAutolink(ref StringSlice text, [NotNullWhen(true)] out string? link, out bool isEmail) - { - link = null; - isEmail = false; + // An absolute URI, for these purposes, consists of a scheme followed by a colon (:) + // followed by zero or more characters other than ASCII whitespace and control characters, <, and >. + // If the URI includes these characters, they must be percent-encoded (e.g. %20 for a space). + // A URI that would end with a full stop (.) is treated instead as ending immediately before the full stop. + + // a scheme is any sequence of 2–32 characters + // beginning with an ASCII letter + // and followed by any combination of ASCII letters, digits, or the symbols plus (”+”), period (”.”), or hyphen (”-”). - var c = text.CurrentChar; - if (c != '<') + // An email address, for these purposes, is anything that matches the non-normative regex from the HTML5 spec: + // /^ + // [a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+ + // @ + // [a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])? + // (?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ + + c = text.NextChar(); + + // -1: scan email + // 0: scan uri or email + // +1: scan uri + int state = 0; + + if (!c.IsAlpha()) + { + // We may have an email char? + if (c.IsDigit() || CharHelper.IsEmailUsernameSpecialChar(c)) + { + state = -1; + } + else { return false; } + } - // An absolute URI, for these purposes, consists of a scheme followed by a colon (:) - // followed by zero or more characters other than ASCII whitespace and control characters, <, and >. - // If the URI includes these characters, they must be percent-encoded (e.g. %20 for a space). - // A URI that would end with a full stop (.) is treated instead as ending immediately before the full stop. - - // a scheme is any sequence of 2–32 characters - // beginning with an ASCII letter - // and followed by any combination of ASCII letters, digits, or the symbols plus (”+”), period (”.”), or hyphen (”-”). - - // An email address, for these purposes, is anything that matches the non-normative regex from the HTML5 spec: - // /^ - // [a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+ - // @ - // [a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])? - // (?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ + var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); + // **************************** + // 1. Scan scheme or user email + // **************************** + builder.Append(c); + while (true) + { c = text.NextChar(); - // -1: scan email - // 0: scan uri or email - // +1: scan uri - int state = 0; - - if (!c.IsAlpha()) + // Chars valid for both scheme and email + var isSpecialChar = c == '+' || c == '.' || c == '-'; + var isValidChar = c.IsAlphaNumeric() || isSpecialChar; + if (state <= 0 && CharHelper.IsEmailUsernameSpecialChar(c)) { - // We may have an email char? - if (c.IsDigit() || CharHelper.IsEmailUsernameSpecialChar(c)) + isValidChar = true; + // If this is not a special char valid also for url scheme, then we have an email + if (!isSpecialChar) { state = -1; } - else + } + + if (isValidChar) + { + // a scheme is any sequence of 2–32 characters + if (state > 0 && builder.Length >= 32) { - return false; + goto ReturnFalse; + } + builder.Append(c); + } + else if (c == ':') + { + if (state < 0 || builder.Length <= 2) + { + goto ReturnFalse; + } + state = 1; + break; + } else if (c == '@') + { + if (state > 0) + { + goto ReturnFalse; } + state = -1; + break; } + else + { + goto ReturnFalse; + } + } - var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); + // append ':' or '@' + builder.Append(c); - // **************************** - // 1. Scan scheme or user email - // **************************** - builder.Append(c); + if (state < 0) + { + isEmail = true; + + // scan an email + // [a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])? + // (?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ + bool hasMinus = false; + int domainCharCount = 0; + char pc = '\0'; while (true) { c = text.NextChar(); - - // Chars valid for both scheme and email - var isSpecialChar = c == '+' || c == '.' || c == '-'; - var isValidChar = c.IsAlphaNumeric() || isSpecialChar; - if (state <= 0 && CharHelper.IsEmailUsernameSpecialChar(c)) + if (c == '>') { - isValidChar = true; - // If this is not a special char valid also for url scheme, then we have an email - if (!isSpecialChar) + if (domainCharCount == 0 || hasMinus) { - state = -1; + break; } + + text.SkipChar(); + link = builder.ToString(); + return true; } - if (isValidChar) + if (c.IsAlphaNumeric() || (domainCharCount > 0 && (hasMinus = c == '-'))) { - // a scheme is any sequence of 2–32 characters - if (state > 0 && builder.Length >= 32) + domainCharCount++; + if (domainCharCount > 63) { - goto ReturnFalse; + break; } - builder.Append(c); } - else if (c == ':') + else if (c == '.') { - if (state < 0 || builder.Length <= 2) + if (pc == '.' || pc == '-') { - goto ReturnFalse; - } - state = 1; - break; - } else if (c == '@') - { - if (state > 0) - { - goto ReturnFalse; + break; } - state = -1; - break; + domainCharCount = 0; + hasMinus = false; } else { - goto ReturnFalse; + break; } + builder.Append(c); + pc = c; } + } + else + { + // scan an uri + // An absolute URI, for these purposes, consists of a scheme followed by a colon (:) + // followed by zero or more characters other than ASCII whitespace and control characters, <, and >. + // If the URI includes these characters, they must be percent-encoded (e.g. %20 for a space). - // append ':' or '@' - builder.Append(c); - - if (state < 0) + while (true) { - isEmail = true; - - // scan an email - // [a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])? - // (?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ - bool hasMinus = false; - int domainCharCount = 0; - char pc = '\0'; - while (true) - { - c = text.NextChar(); - if (c == '>') - { - if (domainCharCount == 0 || hasMinus) - { - break; - } - - text.SkipChar(); - link = builder.ToString(); - return true; - } - - if (c.IsAlphaNumeric() || (domainCharCount > 0 && (hasMinus = c == '-'))) - { - domainCharCount++; - if (domainCharCount > 63) - { - break; - } - } - else if (c == '.') - { - if (pc == '.' || pc == '-') - { - break; - } - domainCharCount = 0; - hasMinus = false; - } - else - { - break; - } - builder.Append(c); - pc = c; + c = text.NextChar(); + if (c == '\0') + { + break; } - } - else - { - // scan an uri - // An absolute URI, for these purposes, consists of a scheme followed by a colon (:) - // followed by zero or more characters other than ASCII whitespace and control characters, <, and >. - // If the URI includes these characters, they must be percent-encoded (e.g. %20 for a space). - while (true) + if (c == '>') { - c = text.NextChar(); - if (c == '\0') - { - break; - } - - if (c == '>') - { - text.SkipChar(); - link = builder.ToString(); - return true; - } + text.SkipChar(); + link = builder.ToString(); + return true; + } - // Chars valid for both scheme and email - if (c <= 127) - { - if (c > ' ' && c != '>') - { - builder.Append(c); - } - else break; - } - else if (!c.IsSpaceOrPunctuation()) + // Chars valid for both scheme and email + if (c <= 127) + { + if (c > ' ' && c != '>') { builder.Append(c); } else break; } + else if (!c.IsSpaceOrPunctuation()) + { + builder.Append(c); + } + else break; } - - ReturnFalse: - builder.Dispose(); - return false; } - public static bool TryParseInlineLink(StringSlice text, out string? link, out string? title) - { - return TryParseInlineLink(ref text, out link, out title, out _, out _); - } + ReturnFalse: + builder.Dispose(); + return false; + } - public static bool TryParseInlineLink(StringSlice text, out string? link, out string? title, out SourceSpan linkSpan, out SourceSpan titleSpan) - { - return TryParseInlineLink(ref text, out link, out title, out linkSpan, out titleSpan); - } + public static bool TryParseInlineLink(StringSlice text, out string? link, out string? title) + { + return TryParseInlineLink(ref text, out link, out title, out _, out _); + } - public static bool TryParseInlineLink(ref StringSlice text, out string? link, out string? title) - { - return TryParseInlineLink(ref text, out link, out title, out SourceSpan linkSpan, out SourceSpan titleSpan); - } + public static bool TryParseInlineLink(StringSlice text, out string? link, out string? title, out SourceSpan linkSpan, out SourceSpan titleSpan) + { + return TryParseInlineLink(ref text, out link, out title, out linkSpan, out titleSpan); + } - public static bool TryParseInlineLink(ref StringSlice text, out string? link, out string? title, out SourceSpan linkSpan, out SourceSpan titleSpan) - { - // 1. An inline link consists of a link text followed immediately by a left parenthesis (, - // 2. optional whitespace, TODO: specs: is it whitespace or multiple whitespaces? - // 3. an optional link destination, - // 4. an optional link title separated from the link destination by whitespace, - // 5. optional whitespace, TODO: specs: is it whitespace or multiple whitespaces? - // 6. and a right parenthesis ) - bool isValid = false; - var c = text.CurrentChar; - link = null; - title = null; + public static bool TryParseInlineLink(ref StringSlice text, out string? link, out string? title) + { + return TryParseInlineLink(ref text, out link, out title, out SourceSpan linkSpan, out SourceSpan titleSpan); + } - linkSpan = SourceSpan.Empty; - titleSpan = SourceSpan.Empty; + public static bool TryParseInlineLink(ref StringSlice text, out string? link, out string? title, out SourceSpan linkSpan, out SourceSpan titleSpan) + { + // 1. An inline link consists of a link text followed immediately by a left parenthesis (, + // 2. optional whitespace, TODO: specs: is it whitespace or multiple whitespaces? + // 3. an optional link destination, + // 4. an optional link title separated from the link destination by whitespace, + // 5. optional whitespace, TODO: specs: is it whitespace or multiple whitespaces? + // 6. and a right parenthesis ) + bool isValid = false; + var c = text.CurrentChar; + link = null; + title = null; + + linkSpan = SourceSpan.Empty; + titleSpan = SourceSpan.Empty; + + // 1. An inline link consists of a link text followed immediately by a left parenthesis (, + if (c == '(') + { + text.SkipChar(); + text.TrimStart(); // this breaks whitespace before an uri - // 1. An inline link consists of a link text followed immediately by a left parenthesis (, - if (c == '(') + var pos = text.Start; + if (TryParseUrl(ref text, out link, out _)) { - text.SkipChar(); - text.TrimStart(); // this breaks whitespace before an uri - - var pos = text.Start; - if (TryParseUrl(ref text, out link, out _)) + linkSpan.Start = pos; + linkSpan.End = text.Start - 1; + if (linkSpan.End < linkSpan.Start) { - linkSpan.Start = pos; - linkSpan.End = text.Start - 1; - if (linkSpan.End < linkSpan.Start) - { - linkSpan = SourceSpan.Empty; - } + linkSpan = SourceSpan.Empty; + } - text.TrimStart(out int spaceCount); - var hasWhiteSpaces = spaceCount > 0; + text.TrimStart(out int spaceCount); + var hasWhiteSpaces = spaceCount > 0; + c = text.CurrentChar; + if (c == ')') + { + isValid = true; + } + else if (hasWhiteSpaces) + { c = text.CurrentChar; + pos = text.Start; if (c == ')') { isValid = true; } - else if (hasWhiteSpaces) + else if (TryParseTitle(ref text, out title, out char enclosingCharacter)) { + titleSpan.Start = pos; + titleSpan.End = text.Start - 1; + if (titleSpan.End < titleSpan.Start) + { + titleSpan = SourceSpan.Empty; + } + text.TrimStart(); c = text.CurrentChar; - pos = text.Start; + if (c == ')') { isValid = true; } - else if (TryParseTitle(ref text, out title, out char enclosingCharacter)) - { - titleSpan.Start = pos; - titleSpan.End = text.Start - 1; - if (titleSpan.End < titleSpan.Start) - { - titleSpan = SourceSpan.Empty; - } - text.TrimStart(); - c = text.CurrentChar; - - if (c == ')') - { - isValid = true; - } - } } } } - - if (isValid) - { - // Skip ')' - text.SkipChar(); - title ??= string.Empty; - } - - return isValid; } - public static bool TryParseInlineLinkTrivia( - ref StringSlice text, - [NotNullWhen(true)] out string? link, - out SourceSpan unescapedLink, - out string? title, - out SourceSpan unescapedTitle, - out char titleEnclosingCharacter, - out SourceSpan linkSpan, - out SourceSpan titleSpan, - out SourceSpan triviaBeforeLink, - out SourceSpan triviaAfterLink, - out SourceSpan triviaAfterTitle, - out bool urlHasPointyBrackets) + if (isValid) { - // 1. An inline link consists of a link text followed immediately by a left parenthesis (, - // 2. optional whitespace, TODO: specs: is it whitespace or multiple whitespaces? - // 3. an optional link destination, - // 4. an optional link title separated from the link destination by whitespace, - // 5. optional whitespace, TODO: specs: is it whitespace or multiple whitespaces? - // 6. and a right parenthesis ) - bool isValid = false; - var c = text.CurrentChar; - link = null; - unescapedLink = SourceSpan.Empty; - title = null; - unescapedTitle = SourceSpan.Empty; + // Skip ')' + text.SkipChar(); + title ??= string.Empty; + } - linkSpan = SourceSpan.Empty; - titleSpan = SourceSpan.Empty; - triviaBeforeLink = SourceSpan.Empty; - triviaAfterLink = SourceSpan.Empty; - triviaAfterTitle = SourceSpan.Empty; - urlHasPointyBrackets = false; - titleEnclosingCharacter = '\0'; + return isValid; + } - // 1. An inline link consists of a link text followed immediately by a left parenthesis (, - if (c == '(') + public static bool TryParseInlineLinkTrivia( + ref StringSlice text, + [NotNullWhen(true)] out string? link, + out SourceSpan unescapedLink, + out string? title, + out SourceSpan unescapedTitle, + out char titleEnclosingCharacter, + out SourceSpan linkSpan, + out SourceSpan titleSpan, + out SourceSpan triviaBeforeLink, + out SourceSpan triviaAfterLink, + out SourceSpan triviaAfterTitle, + out bool urlHasPointyBrackets) + { + // 1. An inline link consists of a link text followed immediately by a left parenthesis (, + // 2. optional whitespace, TODO: specs: is it whitespace or multiple whitespaces? + // 3. an optional link destination, + // 4. an optional link title separated from the link destination by whitespace, + // 5. optional whitespace, TODO: specs: is it whitespace or multiple whitespaces? + // 6. and a right parenthesis ) + bool isValid = false; + var c = text.CurrentChar; + link = null; + unescapedLink = SourceSpan.Empty; + title = null; + unescapedTitle = SourceSpan.Empty; + + linkSpan = SourceSpan.Empty; + titleSpan = SourceSpan.Empty; + triviaBeforeLink = SourceSpan.Empty; + triviaAfterLink = SourceSpan.Empty; + triviaAfterTitle = SourceSpan.Empty; + urlHasPointyBrackets = false; + titleEnclosingCharacter = '\0'; + + // 1. An inline link consists of a link text followed immediately by a left parenthesis (, + if (c == '(') + { + text.SkipChar(); + var sourcePosition = text.Start; + text.TrimStart(); + triviaBeforeLink = new SourceSpan(sourcePosition, text.Start - 1); + var pos = text.Start; + if (TryParseUrlTrivia(ref text, out link, out urlHasPointyBrackets)) { - text.SkipChar(); - var sourcePosition = text.Start; - text.TrimStart(); - triviaBeforeLink = new SourceSpan(sourcePosition, text.Start - 1); - var pos = text.Start; - if (TryParseUrlTrivia(ref text, out link, out urlHasPointyBrackets)) - { - linkSpan.Start = pos; - linkSpan.End = text.Start - 1; - unescapedLink.Start = pos + (urlHasPointyBrackets ? 1 : 0); - unescapedLink.End = text.Start - 1 - (urlHasPointyBrackets ? 1 : 0); - if (linkSpan.End < linkSpan.Start) - { - linkSpan = SourceSpan.Empty; - } + linkSpan.Start = pos; + linkSpan.End = text.Start - 1; + unescapedLink.Start = pos + (urlHasPointyBrackets ? 1 : 0); + unescapedLink.End = text.Start - 1 - (urlHasPointyBrackets ? 1 : 0); + if (linkSpan.End < linkSpan.Start) + { + linkSpan = SourceSpan.Empty; + } - int triviaStart = text.Start; - text.TrimStart(out int spaceCount); + int triviaStart = text.Start; + text.TrimStart(out int spaceCount); - triviaAfterLink = new SourceSpan(triviaStart, text.Start - 1); - var hasWhiteSpaces = spaceCount > 0; + triviaAfterLink = new SourceSpan(triviaStart, text.Start - 1); + var hasWhiteSpaces = spaceCount > 0; + c = text.CurrentChar; + if (c == ')') + { + isValid = true; + } + else if (hasWhiteSpaces) + { c = text.CurrentChar; + pos = text.Start; if (c == ')') { isValid = true; } - else if (hasWhiteSpaces) + else if (TryParseTitleTrivia(ref text, out title, out titleEnclosingCharacter)) { + titleSpan.Start = pos; + titleSpan.End = text.Start - 1; + unescapedTitle.Start = pos + 1; // skip opening character + unescapedTitle.End = text.Start - 1 - 1; // skip closing character + if (titleSpan.End < titleSpan.Start) + { + titleSpan = SourceSpan.Empty; + } + var startTrivia = text.Start; + text.TrimStart(); + triviaAfterTitle = new SourceSpan(startTrivia, text.Start - 1); c = text.CurrentChar; - pos = text.Start; + if (c == ')') { isValid = true; } - else if (TryParseTitleTrivia(ref text, out title, out titleEnclosingCharacter)) - { - titleSpan.Start = pos; - titleSpan.End = text.Start - 1; - unescapedTitle.Start = pos + 1; // skip opening character - unescapedTitle.End = text.Start - 1 - 1; // skip closing character - if (titleSpan.End < titleSpan.Start) - { - titleSpan = SourceSpan.Empty; - } - var startTrivia = text.Start; - text.TrimStart(); - triviaAfterTitle = new SourceSpan(startTrivia, text.Start - 1); - c = text.CurrentChar; - - if (c == ')') - { - isValid = true; - } - } } } } - - if (isValid) - { - // Skip ')' - text.SkipChar(); - title ??= string.Empty; - } - return isValid; } - public static bool TryParseTitle(T text, out string? title) where T : ICharIterator + if (isValid) { - return TryParseTitle(ref text, out title, out _); + // Skip ')' + text.SkipChar(); + title ??= string.Empty; } + return isValid; + } - public static bool TryParseTitle(ref T text, out string? title, out char enclosingCharacter) where T : ICharIterator - { - var buffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); - enclosingCharacter = '\0'; + public static bool TryParseTitle(T text, out string? title) where T : ICharIterator + { + return TryParseTitle(ref text, out title, out _); + } - // a sequence of zero or more characters between straight double-quote characters ("), including a " character only if it is backslash-escaped, or - // a sequence of zero or more characters between straight single-quote characters ('), including a ' character only if it is backslash-escaped, or - var c = text.CurrentChar; - if (c == '\'' || c == '"' || c == '(') + public static bool TryParseTitle(ref T text, out string? title, out char enclosingCharacter) where T : ICharIterator + { + 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 + // a sequence of zero or more characters between straight single-quote characters ('), including a ' character only if it is backslash-escaped, or + var c = text.CurrentChar; + if (c == '\'' || c == '"' || c == '(') + { + enclosingCharacter = c; + var closingQuote = c == '(' ? ')' : c; + bool hasEscape = false; + // -1: undefined + // 0: has only spaces + // 1: has other characters + int hasOnlyWhiteSpacesSinceLastLine = -1; + while (true) { - enclosingCharacter = c; - var closingQuote = c == '(' ? ')' : c; - bool hasEscape = false; - // -1: undefined - // 0: has only spaces - // 1: has other characters - int hasOnlyWhiteSpacesSinceLastLine = -1; - while (true) - { - c = text.NextChar(); - - if (c == '\r' || c == '\n') + c = text.NextChar(); + + if (c == '\r' || c == '\n') + { + if (hasOnlyWhiteSpacesSinceLastLine >= 0) { - if (hasOnlyWhiteSpacesSinceLastLine >= 0) + if (hasOnlyWhiteSpacesSinceLastLine == 1) { - if (hasOnlyWhiteSpacesSinceLastLine == 1) - { - break; - } - hasOnlyWhiteSpacesSinceLastLine = -1; + break; } - buffer.Append(c); - if (c == '\r' && text.PeekChar() == '\n') - { - buffer.Append('\n'); - } - continue; - } - - if (c == '\0') - { - break; + hasOnlyWhiteSpacesSinceLastLine = -1; } - - if (c == closingQuote) + buffer.Append(c); + if (c == '\r' && text.PeekChar() == '\n') { - if (hasEscape) - { - buffer.Append(closingQuote); - hasEscape = false; - continue; - } - - // Skip last quote - text.SkipChar(); - goto ReturnValid; + buffer.Append('\n'); } + continue; + } - if (hasEscape && !c.IsAsciiPunctuation()) - { - buffer.Append('\\'); - } + if (c == '\0') + { + break; + } - if (c == '\\') + if (c == closingQuote) + { + if (hasEscape) { - hasEscape = true; + buffer.Append(closingQuote); + hasEscape = false; continue; } - hasEscape = false; + // Skip last quote + text.SkipChar(); + goto ReturnValid; + } - if (c.IsSpaceOrTab()) - { - if (hasOnlyWhiteSpacesSinceLastLine < 0) - { - hasOnlyWhiteSpacesSinceLastLine = 1; - } - } - else if (c != '\n' && c != '\r' && text.PeekChar() != '\n') + if (hasEscape && !c.IsAsciiPunctuation()) + { + buffer.Append('\\'); + } + + if (c == '\\') + { + hasEscape = true; + continue; + } + + hasEscape = false; + + if (c.IsSpaceOrTab()) + { + if (hasOnlyWhiteSpacesSinceLastLine < 0) { - hasOnlyWhiteSpacesSinceLastLine = 0; + hasOnlyWhiteSpacesSinceLastLine = 1; } - - buffer.Append(c); } + else if (c != '\n' && c != '\r' && text.PeekChar() != '\n') + { + hasOnlyWhiteSpacesSinceLastLine = 0; + } + + buffer.Append(c); } + } - buffer.Dispose(); - title = null; - return false; + buffer.Dispose(); + title = null; + return false; - ReturnValid: - title = buffer.ToString(); - return true; - } + ReturnValid: + title = buffer.ToString(); + return true; + } - public static bool TryParseTitleTrivia(ref T text, out string? title, out char enclosingCharacter) where T : ICharIterator - { - var buffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); - enclosingCharacter = '\0'; + public static bool TryParseTitleTrivia(ref T text, out string? title, out char enclosingCharacter) where T : ICharIterator + { + 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 - // a sequence of zero or more characters between straight single-quote characters ('), including a ' character only if it is backslash-escaped, or - var c = text.CurrentChar; - if (c == '\'' || c == '"' || c == '(') + // a sequence of zero or more characters between straight double-quote characters ("), including a " character only if it is backslash-escaped, or + // a sequence of zero or more characters between straight single-quote characters ('), including a ' character only if it is backslash-escaped, or + var c = text.CurrentChar; + if (c == '\'' || c == '"' || c == '(') + { + enclosingCharacter = c; + var closingQuote = c == '(' ? ')' : c; + bool hasEscape = false; + // -1: undefined + // 0: has only spaces + // 1: has other characters + int hasOnlyWhiteSpacesSinceLastLine = -1; + while (true) { - enclosingCharacter = c; - var closingQuote = c == '(' ? ')' : c; - bool hasEscape = false; - // -1: undefined - // 0: has only spaces - // 1: has other characters - int hasOnlyWhiteSpacesSinceLastLine = -1; - while (true) - { - c = text.NextChar(); - - if (c == '\r' || c == '\n') - { - if (hasOnlyWhiteSpacesSinceLastLine >= 0) - { - if (hasOnlyWhiteSpacesSinceLastLine == 1) - { - break; - } - hasOnlyWhiteSpacesSinceLastLine = -1; - } - buffer.Append(c); - if (c == '\r' && text.PeekChar() == '\n') - { - buffer.Append('\n'); - } - continue; - } - - if (c == '\0') - { - break; - } + c = text.NextChar(); - if (c == closingQuote) + if (c == '\r' || c == '\n') + { + if (hasOnlyWhiteSpacesSinceLastLine >= 0) { - if (hasEscape) + if (hasOnlyWhiteSpacesSinceLastLine == 1) { - buffer.Append(closingQuote); - hasEscape = false; - continue; + break; } - - // Skip last quote - text.SkipChar(); - goto ReturnValid; + hasOnlyWhiteSpacesSinceLastLine = -1; } - - if (hasEscape && !c.IsAsciiPunctuation()) + buffer.Append(c); + if (c == '\r' && text.PeekChar() == '\n') { - buffer.Append('\\'); + buffer.Append('\n'); } + continue; + } - if (c == '\\') + if (c == '\0') + { + break; + } + + if (c == closingQuote) + { + if (hasEscape) { - hasEscape = true; + buffer.Append(closingQuote); + hasEscape = false; continue; } - hasEscape = false; + // Skip last quote + text.SkipChar(); + goto ReturnValid; + } - if (c.IsSpaceOrTab()) - { - if (hasOnlyWhiteSpacesSinceLastLine < 0) - { - hasOnlyWhiteSpacesSinceLastLine = 1; - } - } - else if (c != '\n' && c != '\r' && text.PeekChar() != '\n') + if (hasEscape && !c.IsAsciiPunctuation()) + { + buffer.Append('\\'); + } + + if (c == '\\') + { + hasEscape = true; + continue; + } + + hasEscape = false; + + if (c.IsSpaceOrTab()) + { + if (hasOnlyWhiteSpacesSinceLastLine < 0) { - hasOnlyWhiteSpacesSinceLastLine = 0; + hasOnlyWhiteSpacesSinceLastLine = 1; } - - buffer.Append(c); } + else if (c != '\n' && c != '\r' && text.PeekChar() != '\n') + { + hasOnlyWhiteSpacesSinceLastLine = 0; + } + + buffer.Append(c); } + } - buffer.Dispose(); - title = null; - return false; + buffer.Dispose(); + title = null; + return false; - ReturnValid: - title = buffer.ToString(); - return true; - } + ReturnValid: + title = buffer.ToString(); + return true; + } - public static bool TryParseUrl(T text, [NotNullWhen(true)] out string? link) where T : ICharIterator - { - return TryParseUrl(ref text, out link, out _); - } + public static bool TryParseUrl(T text, [NotNullWhen(true)] out string? link) where T : ICharIterator + { + return TryParseUrl(ref text, out link, out _); + } + + public static bool TryParseUrl(ref T text, [NotNullWhen(true)] out string? link, out bool hasPointyBrackets, bool isAutoLink = false) where T : ICharIterator + { + bool isValid = false; + hasPointyBrackets = false; + var buffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); + + var c = text.CurrentChar; - public static bool TryParseUrl(ref T text, [NotNullWhen(true)] out string? link, out bool hasPointyBrackets, bool isAutoLink = false) where T : ICharIterator + // a sequence of zero or more characters between an opening < and a closing > + // that contains no line breaks, or unescaped < or > characters, or + if (c == '<') { - bool isValid = false; - hasPointyBrackets = false; - var buffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); + bool hasEscape = false; + do + { + c = text.NextChar(); + if (!hasEscape && c == '>') + { + text.SkipChar(); + hasPointyBrackets = true; + isValid = true; + break; + } - var c = text.CurrentChar; + if (!hasEscape && c == '<') + { + break; + } - // a sequence of zero or more characters between an opening < and a closing > - // that contains no line breaks, or unescaped < or > characters, or - if (c == '<') + if (hasEscape && !c.IsAsciiPunctuation()) + { + buffer.Append('\\'); + } + + if (c == '\\') + { + hasEscape = true; + continue; + } + + if (c.IsNewLineOrLineFeed()) + { + break; + } + + hasEscape = false; + + buffer.Append(c); + + } while (c != '\0'); + } + else + { + // a nonempty sequence of characters that does not start with <, does not include ASCII space or control characters, + // and includes parentheses only if (a) they are backslash-escaped or (b) they are part of a + // balanced pair of unescaped parentheses that is not itself inside a balanced pair of unescaped + // parentheses. + bool hasEscape = false; + int openedParent = 0; + while (true) { - bool hasEscape = false; - do + // Match opening and closing parenthesis + if (c == '(') { - c = text.NextChar(); - if (!hasEscape && c == '>') + if (!hasEscape) { - text.SkipChar(); - hasPointyBrackets = true; - isValid = true; - break; + openedParent++; } + } - if (!hasEscape && c == '<') + if (c == ')') + { + if (!hasEscape) { - break; + openedParent--; + if (openedParent < 0) + { + isValid = true; + break; + } } + } + if (!isAutoLink) + { if (hasEscape && !c.IsAsciiPunctuation()) { buffer.Append('\\'); } + // If we have an escape if (c == '\\') { hasEscape = true; + c = text.NextChar(); continue; } - if (c.IsNewLineOrLineFeed()) - { - break; - } - hasEscape = false; + } - buffer.Append(c); + if (IsEndOfUri(c, isAutoLink)) + { + isValid = true; + break; + } - } while (c != '\0'); - } - else - { - // a nonempty sequence of characters that does not start with <, does not include ASCII space or control characters, - // and includes parentheses only if (a) they are backslash-escaped or (b) they are part of a - // balanced pair of unescaped parentheses that is not itself inside a balanced pair of unescaped - // parentheses. - bool hasEscape = false; - int openedParent = 0; - while (true) - { - // Match opening and closing parenthesis - if (c == '(') + if (isAutoLink) + { + if (c == '&') { - if (!hasEscape) + if (HtmlHelper.ScanEntity(text, out _, out _, out _) > 0) { - openedParent++; + isValid = true; + break; } } - - if (c == ')') + if (IsTrailingUrlStopCharacter(c) && IsEndOfUri(text.PeekChar(), true)) { - if (!hasEscape) - { - openedParent--; - if (openedParent < 0) - { - isValid = true; - break; - } - } + isValid = true; + break; } + } - if (!isAutoLink) - { - if (hasEscape && !c.IsAsciiPunctuation()) - { - buffer.Append('\\'); - } + buffer.Append(c); - // If we have an escape - if (c == '\\') - { - hasEscape = true; - c = text.NextChar(); - continue; - } + c = text.NextChar(); + } - hasEscape = false; - } + if (openedParent > 0) + { + isValid = false; + } + } - if (IsEndOfUri(c, isAutoLink)) - { - isValid = true; - break; - } + if (isValid) + { + link = buffer.ToString(); + } + else + { + buffer.Dispose(); + link = null; + } + return isValid; + } - if (isAutoLink) - { - if (c == '&') - { - if (HtmlHelper.ScanEntity(text, out _, out _, out _) > 0) - { - isValid = true; - break; - } - } - if (IsTrailingUrlStopCharacter(c) && IsEndOfUri(text.PeekChar(), true)) - { - isValid = true; - break; - } - } + public static bool TryParseUrlTrivia(ref T text, out string? link, out bool hasPointyBrackets, bool isAutoLink = false) where T : ICharIterator + { + bool isValid = false; + hasPointyBrackets = false; + var buffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); - buffer.Append(c); + var c = text.CurrentChar; - c = text.NextChar(); + // a sequence of zero or more characters between an opening < and a closing > + // that contains no line breaks, or unescaped < or > characters, or + if (c == '<') + { + bool hasEscape = false; + do + { + c = text.NextChar(); + if (!hasEscape && c == '>') + { + text.SkipChar(); + hasPointyBrackets = true; + isValid = true; + break; } - if (openedParent > 0) + if (!hasEscape && c == '<') { - isValid = false; + break; } - } - if (isValid) - { - link = buffer.ToString(); - } - else - { - buffer.Dispose(); - link = null; - } - return isValid; - } + if (hasEscape && !c.IsAsciiPunctuation()) + { + buffer.Append('\\'); + } - public static bool TryParseUrlTrivia(ref T text, out string? link, out bool hasPointyBrackets, bool isAutoLink = false) where T : ICharIterator - { - bool isValid = false; - hasPointyBrackets = false; - var buffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); + if (c == '\\') + { + hasEscape = true; + continue; + } + + if (c.IsNewLineOrLineFeed()) + { + break; + } + + hasEscape = false; - var c = text.CurrentChar; + buffer.Append(c); - // a sequence of zero or more characters between an opening < and a closing > - // that contains no line breaks, or unescaped < or > characters, or - if (c == '<') + } while (c != '\0'); + } + else + { + // a nonempty sequence of characters that does not start with <, does not include ASCII space or control characters, + // and includes parentheses only if (a) they are backslash-escaped or (b) they are part of a + // balanced pair of unescaped parentheses that is not itself inside a balanced pair of unescaped + // parentheses. + bool hasEscape = false; + int openedParent = 0; + while (true) { - bool hasEscape = false; - do + // Match opening and closing parenthesis + if (c == '(') { - c = text.NextChar(); - if (!hasEscape && c == '>') + if (!hasEscape) { - text.SkipChar(); - hasPointyBrackets = true; - isValid = true; - break; + openedParent++; } + } - if (!hasEscape && c == '<') + if (c == ')') + { + if (!hasEscape) { - break; + openedParent--; + if (openedParent < 0) + { + isValid = true; + break; + } } + } + if (!isAutoLink) + { if (hasEscape && !c.IsAsciiPunctuation()) { buffer.Append('\\'); } + // If we have an escape if (c == '\\') { hasEscape = true; + c = text.NextChar(); continue; } - if (c.IsNewLineOrLineFeed()) - { - break; - } - hasEscape = false; + } - buffer.Append(c); - - } while (c != '\0'); - } - else - { - // a nonempty sequence of characters that does not start with <, does not include ASCII space or control characters, - // and includes parentheses only if (a) they are backslash-escaped or (b) they are part of a - // balanced pair of unescaped parentheses that is not itself inside a balanced pair of unescaped - // parentheses. - bool hasEscape = false; - int openedParent = 0; - while (true) - { - // Match opening and closing parenthesis - if (c == '(') - { - if (!hasEscape) - { - openedParent++; - } - } - - if (c == ')') - { - if (!hasEscape) - { - openedParent--; - if (openedParent < 0) - { - isValid = true; - break; - } - } - } + if (IsEndOfUri(c, isAutoLink)) + { + isValid = true; + break; + } - if (!isAutoLink) + if (isAutoLink) + { + if (c == '&') { - if (hasEscape && !c.IsAsciiPunctuation()) + if (HtmlHelper.ScanEntity(text, out _, out _, out _) > 0) { - buffer.Append('\\'); - } - - // If we have an escape - if (c == '\\') - { - hasEscape = true; - c = text.NextChar(); - continue; + isValid = true; + break; } - - hasEscape = false; } - - if (IsEndOfUri(c, isAutoLink)) + if (IsTrailingUrlStopCharacter(c) && IsEndOfUri(text.PeekChar(), true)) { isValid = true; break; } - - if (isAutoLink) - { - if (c == '&') - { - if (HtmlHelper.ScanEntity(text, out _, out _, out _) > 0) - { - isValid = true; - break; - } - } - if (IsTrailingUrlStopCharacter(c) && IsEndOfUri(text.PeekChar(), true)) - { - isValid = true; - break; - } - } - - buffer.Append(c); - - c = text.NextChar(); } - if (openedParent > 0) - { - isValid = false; - } - } + buffer.Append(c); - if (isValid) - { - link = buffer.ToString(); + c = text.NextChar(); } - else + + if (openedParent > 0) { - buffer.Dispose(); - link = null; + isValid = false; } - return isValid; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsTrailingUrlStopCharacter(char c) + if (isValid) { - // Trailing punctuation (specifically, ?, !, ., ,, :, *, _, and ~) will not be considered part of the autolink, though they may be included in the interior of the link: - return c == '?' || c == '!' || c == '.' || c == ',' || c == ':' || c == '*' || c == '*' || c == '_' || c == '~'; + link = buffer.ToString(); } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsEndOfUri(char c, bool isAutoLink) + else { - return c == '\0' || c.IsSpaceOrTab() || c.IsControl() || (isAutoLink && c == '<'); // TODO: specs unclear. space is strict or relaxed? (includes tabs?) + buffer.Dispose(); + link = null; } + return isValid; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsTrailingUrlStopCharacter(char c) + { + // Trailing punctuation (specifically, ?, !, ., ,, :, *, _, and ~) will not be considered part of the autolink, though they may be included in the interior of the link: + return c == '?' || c == '!' || c == '.' || c == ',' || c == ':' || c == '*' || c == '*' || c == '_' || c == '~'; + } - public static bool IsValidDomain(string link, int prefixLength) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsEndOfUri(char c, bool isAutoLink) + { + return c == '\0' || c.IsSpaceOrTab() || c.IsControl() || (isAutoLink && c == '<'); // TODO: specs unclear. space is strict or relaxed? (includes tabs?) + } + + public static bool IsValidDomain(string link, int prefixLength) + { + // https://github.github.com/gfm/#extended-www-autolink + // A valid domain consists of alphanumeric characters, underscores (_), hyphens (-) and periods (.). + // There must be at least one period, and no underscores may be present in the last two segments of the domain. + + // Extended as of https://github.com/lunet-io/markdig/issues/316 to accept non-ascii characters, + // as long as they are not in the space or punctuation categories + + int segmentCount = 1; + bool segmentHasCharacters = false; + int lastUnderscoreSegment = -1; + + for (int i = prefixLength; i < link.Length; i++) { - // https://github.github.com/gfm/#extended-www-autolink - // A valid domain consists of alphanumeric characters, underscores (_), hyphens (-) and periods (.). - // There must be at least one period, and no underscores may be present in the last two segments of the domain. + char c = link[i]; - // Extended as of https://github.com/lunet-io/markdig/issues/316 to accept non-ascii characters, - // as long as they are not in the space or punctuation categories + if (c == '.') // New segment + { + if (!segmentHasCharacters) + return false; - int segmentCount = 1; - bool segmentHasCharacters = false; - int lastUnderscoreSegment = -1; + segmentCount++; + segmentHasCharacters = false; + continue; + } - for (int i = prefixLength; i < link.Length; i++) + if (!c.IsAlphaNumeric()) { - char c = link[i]; + if (c == '/' || c == '?' || c == '#' || c == ':') // End of domain name + break; - if (c == '.') // New segment + if (c == '_') { - if (!segmentHasCharacters) - return false; - - segmentCount++; - segmentHasCharacters = false; - continue; + lastUnderscoreSegment = segmentCount; } - - if (!c.IsAlphaNumeric()) + else if (c != '-' && c.IsSpaceOrPunctuation()) { - if (c == '/' || c == '?' || c == '#' || c == ':') // End of domain name - break; - - if (c == '_') - { - lastUnderscoreSegment = segmentCount; - } - else if (c != '-' && c.IsSpaceOrPunctuation()) - { - // An invalid character has been found - return false; - } + // An invalid character has been found + return false; } - - segmentHasCharacters = true; } - return segmentCount != 1 && // At least one dot was present - segmentHasCharacters && // Last segment has valid characters - segmentCount - lastUnderscoreSegment >= 2; // No underscores are present in the last two segments of the domain + segmentHasCharacters = true; } - public static bool TryParseLinkReferenceDefinition(ref T text, - out string? label, - out string? url, - out string? title, - out SourceSpan labelSpan, - out SourceSpan urlSpan, - out SourceSpan titleSpan) where T : ICharIterator - { - url = null; - title = null; + return segmentCount != 1 && // At least one dot was present + segmentHasCharacters && // Last segment has valid characters + segmentCount - lastUnderscoreSegment >= 2; // No underscores are present in the last two segments of the domain + } - urlSpan = SourceSpan.Empty; - titleSpan = SourceSpan.Empty; + public static bool TryParseLinkReferenceDefinition(ref T text, + out string? label, + out string? url, + out string? title, + out SourceSpan labelSpan, + out SourceSpan urlSpan, + out SourceSpan titleSpan) where T : ICharIterator + { + url = null; + title = null; - if (!TryParseLabel(ref text, out label, out labelSpan)) - { - return false; - } + urlSpan = SourceSpan.Empty; + titleSpan = SourceSpan.Empty; - if (text.CurrentChar != ':') - { - label = null; - return false; - } - text.SkipChar(); // Skip ':' + if (!TryParseLabel(ref text, out label, out labelSpan)) + { + return false; + } - // Skip any whitespace before the url - text.TrimStart(); + if (text.CurrentChar != ':') + { + label = null; + return false; + } + text.SkipChar(); // Skip ':' - urlSpan.Start = text.Start; - bool isAngleBracketsUrl = text.CurrentChar == '<'; - if (!TryParseUrl(ref text, out url, out _) || (!isAngleBracketsUrl && string.IsNullOrEmpty(url))) - { - return false; - } - urlSpan.End = text.Start - 1; + // Skip any whitespace before the url + text.TrimStart(); - var saved = text; - var hasWhiteSpaces = CharIteratorHelper.TrimStartAndCountNewLines(ref text, out int newLineCount); - var c = text.CurrentChar; - if (c == '\'' || c == '"' || c == '(') + urlSpan.Start = text.Start; + bool isAngleBracketsUrl = text.CurrentChar == '<'; + if (!TryParseUrl(ref text, out url, out _) || (!isAngleBracketsUrl && string.IsNullOrEmpty(url))) + { + return false; + } + urlSpan.End = text.Start - 1; + + var saved = text; + var hasWhiteSpaces = CharIteratorHelper.TrimStartAndCountNewLines(ref text, out int newLineCount); + var c = text.CurrentChar; + if (c == '\'' || c == '"' || c == '(') + { + titleSpan.Start = text.Start; + if (TryParseTitle(ref text, out title, out _)) { - titleSpan.Start = text.Start; - if (TryParseTitle(ref text, out title, out _)) - { - titleSpan.End = text.Start - 1; - // If we have a title, it requires a whitespace after the url - if (!hasWhiteSpaces) - { - return false; - } - } - else + titleSpan.End = text.Start - 1; + // If we have a title, it requires a whitespace after the url + if (!hasWhiteSpaces) { return false; } } else { - if (text.IsEmpty || newLineCount > 0) - { - return true; - } - } - - // Check that the current line has only trailing spaces - c = text.CurrentChar; - while (c.IsSpaceOrTab()) - { - c = text.NextChar(); - } - - if (c != '\0' && c != '\n' && c != '\r' && text.PeekChar() != '\n') - { - // If we were able to parse the url but the title doesn't end with space, - // we are still returning a valid definition - if (newLineCount > 0 && title != null) - { - text = saved; - title = null; - return true; - } - - label = null; - url = null; - title = null; return false; } - - if (c == '\r' && text.PeekChar() == '\n') + } + else + { + if (text.IsEmpty || newLineCount > 0) { - text.SkipChar(); + return true; } + } - return true; + // Check that the current line has only trailing spaces + c = text.CurrentChar; + while (c.IsSpaceOrTab()) + { + c = text.NextChar(); } - public static bool TryParseLinkReferenceDefinitionTrivia( - ref T text, - out SourceSpan triviaBeforeLabel, - out string? label, - out SourceSpan labelWithTrivia, - out SourceSpan triviaBeforeUrl, // can contain newline - out string? url, - out SourceSpan unescapedUrl, - out bool urlHasPointyBrackets, - out SourceSpan triviaBeforeTitle, // can contain newline - out string? title, // can contain non-consecutive newlines - out SourceSpan unescapedTitle, - out char titleEnclosingCharacter, - out NewLine newLine, - out SourceSpan triviaAfterTitle, - out SourceSpan labelSpan, - out SourceSpan urlSpan, - out SourceSpan titleSpan) where T : ICharIterator + if (c != '\0' && c != '\n' && c != '\r' && text.PeekChar() != '\n') { - labelWithTrivia = SourceSpan.Empty; - triviaBeforeUrl = SourceSpan.Empty; + // If we were able to parse the url but the title doesn't end with space, + // we are still returning a valid definition + if (newLineCount > 0 && title != null) + { + text = saved; + title = null; + return true; + } + + label = null; url = null; - unescapedUrl = SourceSpan.Empty; - triviaBeforeTitle = SourceSpan.Empty; title = null; - unescapedTitle = SourceSpan.Empty; - newLine = NewLine.None; + return false; + } - urlSpan = SourceSpan.Empty; - titleSpan = SourceSpan.Empty; + if (c == '\r' && text.PeekChar() == '\n') + { + text.SkipChar(); + } - text.TrimStart(); - triviaBeforeLabel = new SourceSpan(0, text.Start - 1); - triviaAfterTitle = SourceSpan.Empty; - urlHasPointyBrackets = false; - titleEnclosingCharacter = '\0'; + return true; + } - labelWithTrivia.Start = text.Start + 1; // skip opening [ - if (!TryParseLabelTrivia(ref text, out label, out labelSpan)) - { - return false; - } - labelWithTrivia.End = text.Start - 2; // skip closing ] and subsequent : + public static bool TryParseLinkReferenceDefinitionTrivia( + ref T text, + out SourceSpan triviaBeforeLabel, + out string? label, + out SourceSpan labelWithTrivia, + out SourceSpan triviaBeforeUrl, // can contain newline + out string? url, + out SourceSpan unescapedUrl, + out bool urlHasPointyBrackets, + out SourceSpan triviaBeforeTitle, // can contain newline + out string? title, // can contain non-consecutive newlines + out SourceSpan unescapedTitle, + out char titleEnclosingCharacter, + out NewLine newLine, + out SourceSpan triviaAfterTitle, + out SourceSpan labelSpan, + out SourceSpan urlSpan, + out SourceSpan titleSpan) where T : ICharIterator + { + labelWithTrivia = SourceSpan.Empty; + triviaBeforeUrl = SourceSpan.Empty; + url = null; + unescapedUrl = SourceSpan.Empty; + triviaBeforeTitle = SourceSpan.Empty; + title = null; + unescapedTitle = SourceSpan.Empty; + newLine = NewLine.None; + + urlSpan = SourceSpan.Empty; + titleSpan = SourceSpan.Empty; + + text.TrimStart(); + triviaBeforeLabel = new SourceSpan(0, text.Start - 1); + triviaAfterTitle = SourceSpan.Empty; + urlHasPointyBrackets = false; + titleEnclosingCharacter = '\0'; + + labelWithTrivia.Start = text.Start + 1; // skip opening [ + if (!TryParseLabelTrivia(ref text, out label, out labelSpan)) + { + return false; + } + labelWithTrivia.End = text.Start - 2; // skip closing ] and subsequent : - if (text.CurrentChar != ':') - { - label = null; - return false; - } - text.SkipChar(); // Skip ':' - var triviaBeforeUrlStart = text.Start; + if (text.CurrentChar != ':') + { + label = null; + return false; + } + text.SkipChar(); // Skip ':' + var triviaBeforeUrlStart = text.Start; - // Skip any whitespace before the url - text.TrimStart(); - triviaBeforeUrl = new SourceSpan(triviaBeforeUrlStart, text.Start - 1); + // Skip any whitespace before the url + text.TrimStart(); + triviaBeforeUrl = new SourceSpan(triviaBeforeUrlStart, text.Start - 1); - urlSpan.Start = text.Start; - bool isAngleBracketsUrl = text.CurrentChar == '<'; - unescapedUrl.Start = text.Start + (isAngleBracketsUrl ? 1 : 0); - if (!TryParseUrlTrivia(ref text, out url, out urlHasPointyBrackets) || (!isAngleBracketsUrl && string.IsNullOrEmpty(url))) - { - return false; - } - urlSpan.End = text.Start - 1; - unescapedUrl.End = text.Start - 1 - (isAngleBracketsUrl ? 1 : 0); - int triviaBeforeTitleStart = text.Start; - - var saved = text; - var hasWhiteSpaces = CharIteratorHelper.TrimStartAndCountNewLines(ref text, out int newLineCount, out newLine); - - // Remove the newline from the trivia (as it may have multiple lines) - var triviaBeforeTitleEnd = text.Start - 1; - triviaBeforeTitle = new SourceSpan(triviaBeforeTitleStart, triviaBeforeTitleEnd); - var c = text.CurrentChar; - if (c == '\'' || c == '"' || c == '(') + urlSpan.Start = text.Start; + bool isAngleBracketsUrl = text.CurrentChar == '<'; + unescapedUrl.Start = text.Start + (isAngleBracketsUrl ? 1 : 0); + if (!TryParseUrlTrivia(ref text, out url, out urlHasPointyBrackets) || (!isAngleBracketsUrl && string.IsNullOrEmpty(url))) + { + return false; + } + urlSpan.End = text.Start - 1; + unescapedUrl.End = text.Start - 1 - (isAngleBracketsUrl ? 1 : 0); + int triviaBeforeTitleStart = text.Start; + + var saved = text; + var hasWhiteSpaces = CharIteratorHelper.TrimStartAndCountNewLines(ref text, out int newLineCount, out newLine); + + // Remove the newline from the trivia (as it may have multiple lines) + var triviaBeforeTitleEnd = text.Start - 1; + triviaBeforeTitle = new SourceSpan(triviaBeforeTitleStart, triviaBeforeTitleEnd); + var c = text.CurrentChar; + if (c == '\'' || c == '"' || c == '(') + { + titleSpan.Start = text.Start; + unescapedTitle.Start = text.Start + 1; // + 1; // skip opening enclosing character + if (TryParseTitleTrivia(ref text, out title, out titleEnclosingCharacter)) { - titleSpan.Start = text.Start; - unescapedTitle.Start = text.Start + 1; // + 1; // skip opening enclosing character - if (TryParseTitleTrivia(ref text, out title, out titleEnclosingCharacter)) - { - titleSpan.End = text.Start - 1; - unescapedTitle.End = text.Start - 1 - 1; // skip closing enclosing character - // If we have a title, it requires a whitespace after the url - if (!hasWhiteSpaces) - { - return false; - } - - // Discard the newline if we have a title - newLine = NewLine.None; - } - else + titleSpan.End = text.Start - 1; + unescapedTitle.End = text.Start - 1 - 1; // skip closing enclosing character + // If we have a title, it requires a whitespace after the url + if (!hasWhiteSpaces) { return false; } + + // Discard the newline if we have a title + newLine = NewLine.None; } else { - if (text.IsEmpty || newLineCount > 0) - { - // If we have an end of line, we need to remove it from the trivia - triviaBeforeTitle.End -= newLine.Length(); - triviaAfterTitle = new SourceSpan(text.Start, text.Start - 1); - return true; - } + return false; } - - // Check that the current line has only trailing spaces - c = text.CurrentChar; - int triviaAfterTitleStart = text.Start; - while (c.IsSpaceOrTab()) + } + else + { + if (text.IsEmpty || newLineCount > 0) { - c = text.NextChar(); + // If we have an end of line, we need to remove it from the trivia + triviaBeforeTitle.End -= newLine.Length(); + triviaAfterTitle = new SourceSpan(text.Start, text.Start - 1); + return true; } + } - if (c != '\0' && c != '\n' && c != '\r' && text.PeekChar() != '\n') - { - // If we were able to parse the url but the title doesn't end with space, - // we are still returning a valid definition - if (newLineCount > 0 && title != null) - { - text = saved; - title = null; - newLine = NewLine.None; - unescapedTitle = SourceSpan.Empty; - triviaAfterTitle = SourceSpan.Empty; - return true; - } + // Check that the current line has only trailing spaces + c = text.CurrentChar; + int triviaAfterTitleStart = text.Start; + while (c.IsSpaceOrTab()) + { + c = text.NextChar(); + } - label = null; - url = null; - unescapedUrl = SourceSpan.Empty; + if (c != '\0' && c != '\n' && c != '\r' && text.PeekChar() != '\n') + { + // If we were able to parse the url but the title doesn't end with space, + // we are still returning a valid definition + if (newLineCount > 0 && title != null) + { + text = saved; title = null; + newLine = NewLine.None; unescapedTitle = SourceSpan.Empty; - return false; - } - triviaAfterTitle = new SourceSpan(triviaAfterTitleStart, text.Start - 1); - if (c != '\0') - { - if (c == '\n') - { - newLine = NewLine.LineFeed; - } - else if (c == '\r' && text.PeekChar() == '\n') - { - newLine = NewLine.CarriageReturnLineFeed; - text.SkipChar(); - } - else if (c == '\r') - { - newLine = NewLine.CarriageReturn; - } + triviaAfterTitle = SourceSpan.Empty; + return true; } - return true; + label = null; + url = null; + unescapedUrl = SourceSpan.Empty; + title = null; + unescapedTitle = SourceSpan.Empty; + return false; } - - public static bool TryParseLabel(T lines, [NotNullWhen(true)] out string? label) where T : ICharIterator + triviaAfterTitle = new SourceSpan(triviaAfterTitleStart, text.Start - 1); + if (c != '\0') { - return TryParseLabel(ref lines, false, out label, out SourceSpan labelSpan); + if (c == '\n') + { + newLine = NewLine.LineFeed; + } + else if (c == '\r' && text.PeekChar() == '\n') + { + newLine = NewLine.CarriageReturnLineFeed; + text.SkipChar(); + } + else if (c == '\r') + { + newLine = NewLine.CarriageReturn; + } } - public static bool TryParseLabel(T lines, [NotNullWhen(true)] out string? label, out SourceSpan labelSpan) where T : ICharIterator - { - return TryParseLabel(ref lines, false, out label, out labelSpan); - } + return true; + } - public static bool TryParseLabel(ref T lines, [NotNullWhen(true)] out string? label) where T : ICharIterator - { - return TryParseLabel(ref lines, false, out label, out SourceSpan labelSpan); - } + public static bool TryParseLabel(T lines, [NotNullWhen(true)] out string? label) where T : ICharIterator + { + return TryParseLabel(ref lines, false, out label, out SourceSpan labelSpan); + } - public static bool TryParseLabel(ref T lines, [NotNullWhen(true)] out string? label, out SourceSpan labelSpan) where T : ICharIterator - { - return TryParseLabel(ref lines, false, out label, out labelSpan); - } + public static bool TryParseLabel(T lines, [NotNullWhen(true)] out string? label, out SourceSpan labelSpan) where T : ICharIterator + { + return TryParseLabel(ref lines, false, out label, out labelSpan); + } + + public static bool TryParseLabel(ref T lines, [NotNullWhen(true)] out string? label) where T : ICharIterator + { + return TryParseLabel(ref lines, false, out label, out SourceSpan labelSpan); + } - public static bool TryParseLabelTrivia(ref T lines, [NotNullWhen(true)] out string? label, out SourceSpan labelSpan) where T : ICharIterator + public static bool TryParseLabel(ref T lines, [NotNullWhen(true)] out string? label, out SourceSpan labelSpan) where T : ICharIterator + { + return TryParseLabel(ref lines, false, out label, out labelSpan); + } + + public static bool TryParseLabelTrivia(ref T lines, [NotNullWhen(true)] out string? label, out SourceSpan labelSpan) where T : ICharIterator + { + return TryParseLabelTrivia(ref lines, false, out label, out labelSpan); + } + + public static bool TryParseLabel(ref T lines, bool allowEmpty, [NotNullWhen(true)] out string? label, out SourceSpan labelSpan) where T : ICharIterator + { + label = null; + char c = lines.CurrentChar; + labelSpan = SourceSpan.Empty; + if (c != '[') { - return TryParseLabelTrivia(ref lines, false, out label, out labelSpan); + return false; } + var buffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); - public static bool TryParseLabel(ref T lines, bool allowEmpty, [NotNullWhen(true)] out string? label, out SourceSpan labelSpan) where T : ICharIterator + var startLabel = -1; + var endLabel = -1; + + bool hasEscape = false; + bool previousWhitespace = true; + bool hasNonWhiteSpace = false; + while (true) { - label = null; - char c = lines.CurrentChar; - labelSpan = SourceSpan.Empty; - if (c != '[') + c = lines.NextChar(); + if (c == '\0') { - return false; + break; } - var buffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); - - var startLabel = -1; - var endLabel = -1; - bool hasEscape = false; - bool previousWhitespace = true; - bool hasNonWhiteSpace = false; - while (true) + if (hasEscape) { - c = lines.NextChar(); - if (c == '\0') + if (c != '[' && c != ']' && c != '\\') { break; } - - if (hasEscape) + } + else + { + if (c == '[') { - if (c != '[' && c != ']' && c != '\\') - { - break; - } + break; } - else - { - if (c == '[') - { - break; - } - if (c == ']') + if (c == ']') + { + lines.SkipChar(); // Skip ] + if (allowEmpty || hasNonWhiteSpace) { - lines.SkipChar(); // Skip ] - if (allowEmpty || hasNonWhiteSpace) + // Remove trailing spaces + for (int i = buffer.Length - 1; i >= 0; i--) { - // Remove trailing spaces - for (int i = buffer.Length - 1; i >= 0; i--) + if (!buffer[i].IsWhitespace()) { - if (!buffer[i].IsWhitespace()) - { - break; - } - buffer.Length = i; - endLabel--; + break; } + buffer.Length = i; + endLabel--; + } - // Only valid if buffer is less than 1000 characters - if (buffer.Length <= 999) + // Only valid if buffer is less than 1000 characters + if (buffer.Length <= 999) + { + labelSpan.Start = startLabel; + labelSpan.End = endLabel; + if (labelSpan.Start > labelSpan.End) { - labelSpan.Start = startLabel; - labelSpan.End = endLabel; - if (labelSpan.Start > labelSpan.End) - { - labelSpan = SourceSpan.Empty; - } - goto ReturnValid; + labelSpan = SourceSpan.Empty; } + goto ReturnValid; } - break; } + break; } + } + + var isWhitespace = c.IsWhitespace(); + if (isWhitespace) + { + // Replace any whitespace by a single ' ' + c = ' '; + } - var isWhitespace = c.IsWhitespace(); - if (isWhitespace) + if (!hasEscape && c == '\\') + { + if (startLabel < 0) { - // Replace any whitespace by a single ' ' - c = ' '; + startLabel = lines.Start; } + hasEscape = true; + } + else + { + hasEscape = false; - if (!hasEscape && c == '\\') + if (!previousWhitespace || !isWhitespace) { if (startLabel < 0) { startLabel = lines.Start; } - hasEscape = true; - } - else - { - hasEscape = false; - - if (!previousWhitespace || !isWhitespace) + endLabel = lines.Start; + buffer.Append(c); + if (!isWhitespace) { - if (startLabel < 0) - { - startLabel = lines.Start; - } - endLabel = lines.Start; - buffer.Append(c); - if (!isWhitespace) - { - hasNonWhiteSpace = true; - } + hasNonWhiteSpace = true; } } - previousWhitespace = isWhitespace; } + previousWhitespace = isWhitespace; + } - buffer.Dispose(); - return false; + buffer.Dispose(); + return false; + + ReturnValid: + label = buffer.ToString(); + return true; + } - ReturnValid: - label = buffer.ToString(); - return true; + public static bool TryParseLabelTrivia(ref T lines, bool allowEmpty, out string? label, out SourceSpan labelSpan) where T : ICharIterator + { + label = null; + char c = lines.CurrentChar; + labelSpan = SourceSpan.Empty; + if (c != '[') + { + return false; } + var buffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); - public static bool TryParseLabelTrivia(ref T lines, bool allowEmpty, out string? label, out SourceSpan labelSpan) where T : ICharIterator + var startLabel = -1; + var endLabel = -1; + + bool hasEscape = false; + bool previousWhitespace = true; + bool hasNonWhiteSpace = false; + while (true) { - label = null; - char c = lines.CurrentChar; - labelSpan = SourceSpan.Empty; - if (c != '[') + c = lines.NextChar(); + if (c == '\0') { - return false; + break; } - var buffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); - - var startLabel = -1; - var endLabel = -1; - bool hasEscape = false; - bool previousWhitespace = true; - bool hasNonWhiteSpace = false; - while (true) + if (hasEscape) { - c = lines.NextChar(); - if (c == '\0') + if (c != '[' && c != ']' && c != '\\') { break; } - - if (hasEscape) + } + else + { + if (c == '[') { - if (c != '[' && c != ']' && c != '\\') - { - break; - } + break; } - else - { - if (c == '[') - { - break; - } - if (c == ']') + if (c == ']') + { + lines.SkipChar(); // Skip ] + if (allowEmpty || hasNonWhiteSpace) { - lines.SkipChar(); // Skip ] - if (allowEmpty || hasNonWhiteSpace) + // Remove trailing spaces + for (int i = buffer.Length - 1; i >= 0; i--) { - // Remove trailing spaces - for (int i = buffer.Length - 1; i >= 0; i--) + if (!buffer[i].IsWhitespace()) { - if (!buffer[i].IsWhitespace()) - { - break; - } - buffer.Length = i; - endLabel--; + break; } + buffer.Length = i; + endLabel--; + } - // Only valid if buffer is less than 1000 characters - if (buffer.Length <= 999) + // Only valid if buffer is less than 1000 characters + if (buffer.Length <= 999) + { + labelSpan.Start = startLabel; + labelSpan.End = endLabel; + if (labelSpan.Start > labelSpan.End) { - labelSpan.Start = startLabel; - labelSpan.End = endLabel; - if (labelSpan.Start > labelSpan.End) - { - labelSpan = SourceSpan.Empty; - } - goto ReturnValid; + labelSpan = SourceSpan.Empty; } + goto ReturnValid; } - break; } + break; } + } + + var isWhitespace = c.IsWhitespace(); - var isWhitespace = c.IsWhitespace(); + if (!hasEscape && c == '\\') + { + if (startLabel < 0) + { + startLabel = lines.Start; + } + hasEscape = true; + } + else + { + hasEscape = false; - if (!hasEscape && c == '\\') + if (!previousWhitespace || !isWhitespace) { if (startLabel < 0) { startLabel = lines.Start; } - hasEscape = true; - } - else - { - hasEscape = false; - - if (!previousWhitespace || !isWhitespace) + endLabel = lines.Start; + if (isWhitespace) { - if (startLabel < 0) - { - startLabel = lines.Start; - } - endLabel = lines.Start; - if (isWhitespace) - { - // Replace any whitespace by a single ' ' - buffer.Append(' '); - } - else - { - buffer.Append(c); - } - if (!isWhitespace) - { - hasNonWhiteSpace = true; - } + // Replace any whitespace by a single ' ' + buffer.Append(' '); + } + else + { + buffer.Append(c); + } + if (!isWhitespace) + { + hasNonWhiteSpace = true; } } - previousWhitespace = isWhitespace; } + previousWhitespace = isWhitespace; + } - buffer.Dispose(); - return false; + buffer.Dispose(); + return false; - ReturnValid: - label = buffer.ToString(); - return true; - } + ReturnValid: + label = buffer.ToString(); + return true; } } \ No newline at end of file diff --git a/src/Markdig/Helpers/Newline.cs b/src/Markdig/Helpers/Newline.cs index b3c25e8e0..68f28bbea 100644 --- a/src/Markdig/Helpers/Newline.cs +++ b/src/Markdig/Helpers/Newline.cs @@ -2,34 +2,31 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using System; +namespace Markdig.Helpers; -namespace Markdig.Helpers +/// +/// Represents a character or set of characters that represent a separation +/// between two lines of text +/// +public enum NewLine : byte { - /// - /// Represents a character or set of characters that represent a separation - /// between two lines of text - /// - public enum NewLine : byte - { - // Values have the length encoded in last 2 bits - None = 0, - CarriageReturn = 4 | 1, - LineFeed = 8 | 1, - CarriageReturnLineFeed = 16 | 2 - } + // Values have the length encoded in last 2 bits + None = 0, + CarriageReturn = 4 | 1, + LineFeed = 8 | 1, + CarriageReturnLineFeed = 16 | 2 +} - public static class NewLineExtensions +public static class NewLineExtensions +{ + public static string AsString(this NewLine newLine) => newLine switch { - public static string AsString(this NewLine newLine) => newLine switch - { - NewLine.CarriageReturnLineFeed => "\r\n", - NewLine.LineFeed => "\n", - NewLine.CarriageReturn => "\r", - _ => string.Empty, - }; + NewLine.CarriageReturnLineFeed => "\r\n", + NewLine.LineFeed => "\n", + NewLine.CarriageReturn => "\r", + _ => string.Empty, + }; - public static int Length(this NewLine newLine) => (int)newLine & 3; - } + public static int Length(this NewLine newLine) => (int)newLine & 3; } diff --git a/src/Markdig/Helpers/ObjectCache.cs b/src/Markdig/Helpers/ObjectCache.cs index 91614369d..9710ae033 100644 --- a/src/Markdig/Helpers/ObjectCache.cs +++ b/src/Markdig/Helpers/ObjectCache.cs @@ -2,71 +2,69 @@ // 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.Collections.Concurrent; -namespace Markdig.Helpers +namespace Markdig.Helpers; + +/// +/// A simple object recycling system. +/// +/// Type of the object to cache +public abstract class ObjectCache where T : class { + private readonly ConcurrentQueue _builders; + /// - /// A simple object recycling system. + /// Initializes a new instance of the class. /// - /// Type of the object to cache - public abstract class ObjectCache where T : class + protected ObjectCache() { - private readonly ConcurrentQueue _builders; + _builders = new ConcurrentQueue(); + } - /// - /// Initializes a new instance of the class. - /// - protected ObjectCache() - { - _builders = new ConcurrentQueue(); - } + /// + /// Clears this cache. + /// + public void Clear() + { + _builders.Clear(); + } - /// - /// Clears this cache. - /// - public void Clear() + /// + /// Gets a new instance. + /// + /// + public T Get() + { + if (_builders.TryDequeue(out T instance)) { - _builders.Clear(); + return instance; } - /// - /// Gets a new instance. - /// - /// - public T Get() - { - if (_builders.TryDequeue(out T instance)) - { - return instance; - } - - return NewInstance(); - } + return NewInstance(); + } - /// - /// Releases the specified instance. - /// - /// The instance. - /// if instance is null - public void Release(T instance) - { - if (instance is null) ThrowHelper.ArgumentNullException(nameof(instance)); - Reset(instance); - _builders.Enqueue(instance); - } + /// + /// Releases the specified instance. + /// + /// The instance. + /// if instance is null + public void Release(T instance) + { + if (instance is null) ThrowHelper.ArgumentNullException(nameof(instance)); + Reset(instance); + _builders.Enqueue(instance); + } - /// - /// Creates a new instance of {T} - /// - /// A new instance of {T} - protected abstract T NewInstance(); + /// + /// Creates a new instance of {T} + /// + /// A new instance of {T} + protected abstract T NewInstance(); - /// - /// Resets the specified instance when is called before storing back to this cache. - /// - /// The instance. - protected abstract void Reset(T instance); - } + /// + /// Resets the specified instance when is called before storing back to this cache. + /// + /// The instance. + protected abstract void Reset(T instance); } \ No newline at end of file diff --git a/src/Markdig/Helpers/OrderedList.cs b/src/Markdig/Helpers/OrderedList.cs index 26e4d5963..69a3241fc 100644 --- a/src/Markdig/Helpers/OrderedList.cs +++ b/src/Markdig/Helpers/OrderedList.cs @@ -2,162 +2,160 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -namespace Markdig.Helpers +namespace Markdig.Helpers; + +/// +/// A List that provides methods for inserting/finding before/after. See remarks. +/// +/// Type of the list item +/// +/// We use a typed list and don't use extension methods because it would pollute all list implements and the top level namespace. +public class OrderedList : List where T: notnull { - /// - /// A List that provides methods for inserting/finding before/after. See remarks. - /// - /// Type of the list item - /// - /// We use a typed list and don't use extension methods because it would pollute all list implements and the top level namespace. - public class OrderedList : List where T: notnull + public OrderedList() { - public OrderedList() - { - } + } - public OrderedList(IEnumerable collection) : base(collection) - { - } + public OrderedList(IEnumerable collection) : base(collection) + { + } - public bool InsertBefore(T item) where TItem : T + public bool InsertBefore(T item) where TItem : T + { + if (item is null) ThrowHelper.ArgumentNullException_item(); + for (int i = 0; i < Count; i++) { - if (item is null) ThrowHelper.ArgumentNullException_item(); - for (int i = 0; i < Count; i++) + if (this[i] is TItem) { - if (this[i] is TItem) - { - Insert(i, item); - return true; - } + Insert(i, item); + return true; } - return false; } + return false; + } - public TItem? Find() where TItem : T + public TItem? Find() where TItem : T + { + for (int i = 0; i < Count; i++) { - for (int i = 0; i < Count; i++) + if (this[i] is TItem) { - if (this[i] is TItem) - { - return (TItem)this[i]; - } + return (TItem)this[i]; } - return default; } + return default; + } - public bool TryFind([NotNullWhen(true)] out TItem? item) where TItem : T - { - item = Find(); - return item != null; - } + public bool TryFind([NotNullWhen(true)] out TItem? item) where TItem : T + { + item = Find(); + return item != null; + } - public TItem? FindExact() where TItem : T + public TItem? FindExact() where TItem : T + { + for (int i = 0; i < Count; i++) { - for (int i = 0; i < Count; i++) + if (this[i].GetType() == typeof(TItem)) { - if (this[i].GetType() == typeof(TItem)) - { - return (TItem)this[i]; - } + return (TItem)this[i]; } - return default; } + return default; + } - public void AddIfNotAlready() where TItem : class, T, new() + public void AddIfNotAlready() where TItem : class, T, new() + { + if (!Contains()) { - if (!Contains()) - { - Add(new TItem()); - } + Add(new TItem()); } + } - public void AddIfNotAlready(TItem item) where TItem : T + public void AddIfNotAlready(TItem item) where TItem : T + { + if (!Contains()) { - if (!Contains()) - { - Add(item); - } + Add(item); } + } - public bool InsertAfter(T item) where TItem : T + public bool InsertAfter(T item) where TItem : T + { + if (item is null) ThrowHelper.ArgumentNullException_item(); + for (int i = 0; i < Count; i++) { - if (item is null) ThrowHelper.ArgumentNullException_item(); - for (int i = 0; i < Count; i++) + if (this[i] is TItem) { - if (this[i] is TItem) - { - Insert(i + 1, item); - return true; - } + Insert(i + 1, item); + return true; } - return false; } + return false; + } - public bool Contains() where TItem : T + public bool Contains() where TItem : T + { + for (int i = 0; i < Count; i++) { - for (int i = 0; i < Count; i++) + if (this[i] is TItem) { - if (this[i] is TItem) - { - return true; - } + return true; } - return false; } + return false; + } - /// - /// Replaces with . - /// - /// Item type to find in the list - /// Object to replace this item with - /// true if a replacement was made; otherwise false. - public bool Replace(T replacement) where TItem : T + /// + /// Replaces with . + /// + /// Item type to find in the list + /// Object to replace this item with + /// true if a replacement was made; otherwise false. + public bool Replace(T replacement) where TItem : T + { + for (var i = 0; i < Count; i++) { - for (var i = 0; i < Count; i++) + if (this[i] is TItem) { - if (this[i] is TItem) - { - RemoveAt(i); - Insert(i, replacement); - return true; - } + RemoveAt(i); + Insert(i, replacement); + return true; } - return false; } + return false; + } - /// - /// Replaces with or adds . - /// - /// Item type to find in the list - /// Object to add/replace the found item with - /// true if a replacement was made; otherwise false. - public bool ReplaceOrAdd(T newItem) where TItem : T - { - if (Replace(newItem)) - return true; + /// + /// Replaces with or adds . + /// + /// Item type to find in the list + /// Object to add/replace the found item with + /// true if a replacement was made; otherwise false. + public bool ReplaceOrAdd(T newItem) where TItem : T + { + if (Replace(newItem)) + return true; - Add(newItem); - return false; - } + Add(newItem); + return false; + } - /// - /// Removes the first occurrence of - /// - public bool TryRemove() where TItem : T + /// + /// Removes the first occurrence of + /// + public bool TryRemove() where TItem : T + { + for (int i = 0; i < Count; i++) { - for (int i = 0; i < Count; i++) + if (this[i] is TItem) { - if (this[i] is TItem) - { - RemoveAt(i); - return true; - } + RemoveAt(i); + return true; } - return false; } + return false; } } \ No newline at end of file diff --git a/src/Markdig/Helpers/StringBuilderCache.cs b/src/Markdig/Helpers/StringBuilderCache.cs index b467fbacb..1c25ac7b6 100644 --- a/src/Markdig/Helpers/StringBuilderCache.cs +++ b/src/Markdig/Helpers/StringBuilderCache.cs @@ -2,31 +2,29 @@ // 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.Text; -namespace Markdig.Helpers +namespace Markdig.Helpers; + +public static class StringBuilderCache { - public static class StringBuilderCache - { - /// - /// A StringBuilder that can be used locally in a method body only. - /// - [ThreadStatic] - private static StringBuilder? local; + /// + /// A StringBuilder that can be used locally in a method body only. + /// + [ThreadStatic] + private static StringBuilder? local; - /// - /// Provides a string builder that can only be used locally in a method. This StringBuilder MUST not be stored. - /// - /// - public static StringBuilder Local() + /// + /// Provides a string builder that can only be used locally in a method. This StringBuilder MUST not be stored. + /// + /// + public static StringBuilder Local() + { + var sb = local ??= new StringBuilder(); + if (sb.Length != 0) { - var sb = local ??= new StringBuilder(); - if (sb.Length != 0) - { - sb.Length = 0; - } - return sb; + sb.Length = 0; } + return sb; } } \ No newline at end of file diff --git a/src/Markdig/Helpers/StringBuilderExtensions.cs b/src/Markdig/Helpers/StringBuilderExtensions.cs index 247336a69..f9d5d144e 100644 --- a/src/Markdig/Helpers/StringBuilderExtensions.cs +++ b/src/Markdig/Helpers/StringBuilderExtensions.cs @@ -4,21 +4,20 @@ using System.Text; -namespace Markdig.Helpers +namespace Markdig.Helpers; + +/// +/// Extensions for StringBuilder +/// +public static class StringBuilderExtensions { /// - /// Extensions for StringBuilder + /// Appends the specified slice to this instance. /// - public static class StringBuilderExtensions + /// The builder. + /// The slice. + public static StringBuilder Append(this StringBuilder builder, StringSlice slice) { - /// - /// Appends the specified slice to this instance. - /// - /// The builder. - /// The slice. - public static StringBuilder Append(this StringBuilder builder, StringSlice slice) - { - return builder.Append(slice.Text, slice.Start, slice.Length); - } + return builder.Append(slice.Text, slice.Start, slice.Length); } } \ No newline at end of file diff --git a/src/Markdig/Helpers/StringLine.cs b/src/Markdig/Helpers/StringLine.cs index 22948d8ba..fcd28c964 100644 --- a/src/Markdig/Helpers/StringLine.cs +++ b/src/Markdig/Helpers/StringLine.cs @@ -2,97 +2,96 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -namespace Markdig.Helpers +namespace Markdig.Helpers; + +/// +/// A struct representing a text line. +/// +public struct StringLine { /// - /// A struct representing a text line. + /// Initializes a new instance of the struct. /// - public struct StringLine + /// The slice. + public StringLine(ref StringSlice slice) : this() { - /// - /// Initializes a new instance of the struct. - /// - /// The slice. - public StringLine(ref StringSlice slice) : this() - { - Slice = slice; - NewLine = slice.NewLine; - } + Slice = slice; + NewLine = slice.NewLine; + } - /// - /// Initializes a new instance of the struct. - /// - /// The slice. - /// The line. - /// The column. - /// The position. - /// The line separation. - public StringLine(StringSlice slice, int line, int column, int position, NewLine newLine) - { - Slice = slice; - Line = line; - Column = column; - Position = position; - NewLine = newLine; - } + /// + /// Initializes a new instance of the struct. + /// + /// The slice. + /// The line. + /// The column. + /// The position. + /// The line separation. + public StringLine(StringSlice slice, int line, int column, int position, NewLine newLine) + { + Slice = slice; + Line = line; + Column = column; + Position = position; + NewLine = newLine; + } - /// - /// Initializes a new instance of the struct. - /// - /// The slice. - /// The line. - /// The column. - /// The position. - /// The line separation. - public StringLine(ref StringSlice slice, int line, int column, int position, NewLine newLine) - { - Slice = slice; - Line = line; - Column = column; - Position = position; - NewLine = newLine; - } + /// + /// Initializes a new instance of the struct. + /// + /// The slice. + /// The line. + /// The column. + /// The position. + /// The line separation. + public StringLine(ref StringSlice slice, int line, int column, int position, NewLine newLine) + { + Slice = slice; + Line = line; + Column = column; + Position = position; + NewLine = newLine; + } - /// - /// The slice used for this line. - /// - public StringSlice Slice; + /// + /// The slice used for this line. + /// + public StringSlice Slice; - /// - /// The line position. - /// - public int Line; + /// + /// The line position. + /// + public int Line; - /// - /// The position of the start of this line within the original source code - /// - public int Position; + /// + /// The position of the start of this line within the original source code + /// + public int Position; - /// - /// The column position. - /// - public int Column; + /// + /// The column position. + /// + public int Column; - /// - /// The newline. - /// - public NewLine NewLine; + /// + /// The newline. + /// + public NewLine NewLine; - /// - /// Performs an implicit conversion from to . - /// - /// The line. - /// - /// The result of the conversion. - /// - public static implicit operator StringSlice(StringLine line) - { - return line.Slice; - } + /// + /// Performs an implicit conversion from to . + /// + /// The line. + /// + /// The result of the conversion. + /// + public static implicit operator StringSlice(StringLine line) + { + return line.Slice; + } - public readonly override string ToString() - { - return Slice.ToString(); - } + public readonly override string ToString() + { + return Slice.ToString(); } } \ No newline at end of file diff --git a/src/Markdig/Helpers/StringLineGroup.cs b/src/Markdig/Helpers/StringLineGroup.cs index b86e82e98..35f094a9b 100644 --- a/src/Markdig/Helpers/StringLineGroup.cs +++ b/src/Markdig/Helpers/StringLineGroup.cs @@ -2,446 +2,443 @@ // 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.Collections; -using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; -namespace Markdig.Helpers +namespace Markdig.Helpers; + +/// +/// A group of . +/// +/// +public struct StringLineGroup : IEnumerable { + // Feel free to change these numbers if you see a positive change + private static readonly CustomArrayPool _pool + = new CustomArrayPool(512, 386, 128, 64); + /// - /// A group of . + /// Initializes a new instance of the class. /// - /// - public struct StringLineGroup : IEnumerable + /// + public StringLineGroup(int capacity) { - // Feel free to change these numbers if you see a positive change - private static readonly CustomArrayPool _pool - = new CustomArrayPool(512, 386, 128, 64); - - /// - /// Initializes a new instance of the class. - /// - /// - public StringLineGroup(int capacity) - { - if (capacity <= 0) ThrowHelper.ArgumentOutOfRangeException(nameof(capacity)); - Lines = _pool.Rent(capacity); - Count = 0; - } + if (capacity <= 0) ThrowHelper.ArgumentOutOfRangeException(nameof(capacity)); + Lines = _pool.Rent(capacity); + Count = 0; + } - internal StringLineGroup(int capacity, bool willRelease) - { - if (capacity <= 0) ThrowHelper.ArgumentOutOfRangeException(nameof(capacity)); - Lines = _pool.Rent(willRelease ? Math.Max(8, capacity) : capacity); - Count = 0; - } + internal StringLineGroup(int capacity, bool willRelease) + { + if (capacity <= 0) ThrowHelper.ArgumentOutOfRangeException(nameof(capacity)); + Lines = _pool.Rent(willRelease ? Math.Max(8, capacity) : capacity); + Count = 0; + } - /// - /// Initializes a new instance of the class. - /// - /// The text. - /// - public StringLineGroup(string text) - { - if (text is null) ThrowHelper.ArgumentNullException_text(); - Lines = new StringLine[1]; - Count = 0; - Add(new StringSlice(text)); - } + /// + /// Initializes a new instance of the class. + /// + /// The text. + /// + public StringLineGroup(string text) + { + if (text is null) ThrowHelper.ArgumentNullException_text(); + Lines = new StringLine[1]; + Count = 0; + Add(new StringSlice(text)); + } - /// - /// Gets the lines. - /// - public StringLine[] Lines { get; private set; } + /// + /// Gets the lines. + /// + public StringLine[] Lines { get; private set; } - /// - /// Gets the number of lines. - /// - public int Count { get; private set; } + /// + /// Gets the number of lines. + /// + public int Count { get; private set; } - /// - /// Clears this instance. - /// - public void Clear() + /// + /// Clears this instance. + /// + public void Clear() + { + Array.Clear(Lines, 0, Lines.Length); + Count = 0; + } + + /// + /// Removes the line at the specified index. + /// + /// The index. + public void RemoveAt(int index) + { + if (index != Count - 1) { - Array.Clear(Lines, 0, Lines.Length); - Count = 0; + Array.Copy(Lines, index + 1, Lines, index, Count - index - 1); } - /// - /// Removes the line at the specified index. - /// - /// The index. - public void RemoveAt(int index) - { - if (index != Count - 1) - { - Array.Copy(Lines, index + 1, Lines, index, Count - index - 1); - } + Lines[Count - 1] = new StringLine(); + Count--; + } - Lines[Count - 1] = new StringLine(); - Count--; - } + internal void RemoveStartRange(int toRemove) + { + int remaining = Count - toRemove; + Count = remaining; + Array.Copy(Lines, toRemove, Lines, 0, remaining); + Array.Clear(Lines, remaining, toRemove); + } - internal void RemoveStartRange(int toRemove) - { - int remaining = Count - toRemove; - Count = remaining; - Array.Copy(Lines, toRemove, Lines, 0, remaining); - Array.Clear(Lines, remaining, toRemove); - } + /// + /// Adds the specified line to this instance. + /// + /// The line. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(ref StringLine line) + { + if (Count == Lines.Length) IncreaseCapacity(); + Lines[Count++] = line; + } - /// - /// Adds the specified line to this instance. - /// - /// The line. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Add(ref StringLine line) + /// + /// Adds the specified slice to this instance. + /// + /// The slice. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(StringSlice slice) + { + if (Count == Lines.Length) IncreaseCapacity(); + Lines[Count++] = new StringLine(ref slice); + } + + public readonly override string ToString() + { + return ToSlice().ToString(); + } + + /// + /// Converts the lines to a single by concatenating the lines. + /// + /// The position of the `\n` line offsets from the beginning of the returned slice. + /// A single slice concatenating the lines of this instance + public readonly StringSlice ToSlice(List? lineOffsets = null) + { + // Optimization case for a single line. + if (Count == 1) { - if (Count == Lines.Length) IncreaseCapacity(); - Lines[Count++] = line; + ref StringLine line = ref Lines[0]; + lineOffsets?.Add(new LineOffset(line.Position, line.Column, line.Slice.Start - line.Position, line.Slice.Start, line.Slice.End + 1)); + return Lines[0]; } - /// - /// Adds the specified slice to this instance. - /// - /// The slice. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Add(StringSlice slice) + // Optimization case when no lines + if (Count == 0) { - if (Count == Lines.Length) IncreaseCapacity(); - Lines[Count++] = new StringLine(ref slice); + return StringSlice.Empty; } - public readonly override string ToString() + if (lineOffsets != null && lineOffsets.Capacity < lineOffsets.Count + Count) { - return ToSlice().ToString(); + lineOffsets.Capacity = Math.Max(lineOffsets.Count + Count, lineOffsets.Capacity * 2); } - /// - /// Converts the lines to a single by concatenating the lines. - /// - /// The position of the `\n` line offsets from the beginning of the returned slice. - /// A single slice concatenating the lines of this instance - public readonly StringSlice ToSlice(List? lineOffsets = null) + // Else use a builder + var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); + int previousStartOfLine = 0; + var newLine = NewLine.None; + for (int i = 0; i < Count; i++) { - // Optimization case for a single line. - if (Count == 1) - { - ref StringLine line = ref Lines[0]; - lineOffsets?.Add(new LineOffset(line.Position, line.Column, line.Slice.Start - line.Position, line.Slice.Start, line.Slice.End + 1)); - return Lines[0]; - } - - // Optimization case when no lines - if (Count == 0) + if (i > 0) { - return StringSlice.Empty; + builder.Append(newLine.AsString()); + previousStartOfLine = builder.Length; } - - if (lineOffsets != null && lineOffsets.Capacity < lineOffsets.Count + Count) + ref StringLine line = ref Lines[i]; + if (!line.Slice.IsEmpty) { - lineOffsets.Capacity = Math.Max(lineOffsets.Count + Count, lineOffsets.Capacity * 2); + builder.Append(line.Slice.AsSpan()); } + newLine = line.NewLine; - // Else use a builder - var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); - int previousStartOfLine = 0; - var newLine = NewLine.None; - for (int i = 0; i < Count; i++) - { - if (i > 0) - { - builder.Append(newLine.AsString()); - previousStartOfLine = builder.Length; - } - ref StringLine line = ref Lines[i]; - if (!line.Slice.IsEmpty) - { - 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.ToString()); + lineOffsets?.Add(new LineOffset(line.Position, line.Column, line.Slice.Start - line.Position, previousStartOfLine, builder.Length)); } + return new StringSlice(builder.ToString()); + } - /// - /// Converts this instance into a . - /// - /// - public readonly Iterator ToCharIterator() - { - return new Iterator(this); - } + /// + /// Converts this instance into a . + /// + /// + public readonly Iterator ToCharIterator() + { + return new Iterator(this); + } - /// - /// Trims each lines of the specified . - /// - public void Trim() + /// + /// Trims each lines of the specified . + /// + public void Trim() + { + for (int i = 0; i < Count; i++) { - for (int i = 0; i < Count; i++) - { - Lines[i].Slice.Trim(); - } + Lines[i].Slice.Trim(); } + } - IEnumerator IEnumerable.GetEnumerator() + IEnumerator IEnumerable.GetEnumerator() + { + return Lines.GetEnumerator(); + } + + private void IncreaseCapacity() + { + var newItems = _pool.Rent(Lines.Length * 2); + if (Count > 0) { - return Lines.GetEnumerator(); + Array.Copy(Lines, 0, newItems, 0, Count); + Array.Clear(Lines, 0, Count); } + _pool.Return(Lines); + Lines = newItems; + } + + internal void Release() + { + Array.Clear(Lines, 0, Count); + _pool.Return(Lines); + Lines = null!; + Count = -1; + } + + /// + /// The iterator used to iterate other the lines. + /// + /// + public struct Iterator : ICharIterator + { + private readonly StringLineGroup _lines; + private StringSlice _currentSlice; + private int _offset; - private void IncreaseCapacity() + public Iterator(StringLineGroup stringLineGroup) { - var newItems = _pool.Rent(Lines.Length * 2); - if (Count > 0) + _lines = stringLineGroup; + Start = -1; + _offset = -1; + SliceIndex = 0; + CurrentChar = '\0'; + End = -1; + StringLine[] lines = stringLineGroup.Lines; + for (int i = 0; i < stringLineGroup.Count && i < lines.Length; i++) { - Array.Copy(Lines, 0, newItems, 0, Count); - Array.Clear(Lines, 0, Count); + ref StringSlice slice = ref lines[i].Slice; + End += slice.Length + slice.NewLine.Length(); // Add chars } - _pool.Return(Lines); - Lines = newItems; + _currentSlice = _lines.Lines[0].Slice; + SkipChar(); } - internal void Release() - { - Array.Clear(Lines, 0, Count); - _pool.Return(Lines); - Lines = null!; - Count = -1; - } + public int Start { get; private set; } - /// - /// The iterator used to iterate other the lines. - /// - /// - public struct Iterator : ICharIterator - { - private readonly StringLineGroup _lines; - private StringSlice _currentSlice; - private int _offset; + public char CurrentChar { get; private set; } - public Iterator(StringLineGroup stringLineGroup) - { - _lines = stringLineGroup; - Start = -1; - _offset = -1; - SliceIndex = 0; - CurrentChar = '\0'; - End = -1; - StringLine[] lines = stringLineGroup.Lines; - for (int i = 0; i < stringLineGroup.Count && i < lines.Length; i++) - { - ref StringSlice slice = ref lines[i].Slice; - End += slice.Length + slice.NewLine.Length(); // Add chars - } - _currentSlice = _lines.Lines[0].Slice; - SkipChar(); - } + public int End { get; private set; } - public int Start { get; private set; } + public readonly bool IsEmpty => Start > End; - public char CurrentChar { get; private set; } + public int SliceIndex { get; private set; } - public int End { get; private set; } + public StringLineGroup Remaining() + { + StringLineGroup lines = _lines; + if (IsEmpty) + { + lines.Clear(); + } + else + { + lines.RemoveStartRange(SliceIndex); - public readonly bool IsEmpty => Start > End; + if (lines.Count > 0 && _offset > 0) + { + ref StringLine line = ref lines.Lines[0]; + line.Column += _offset; + line.Slice.Start += _offset; + } + } - public int SliceIndex { get; private set; } + return lines; + } - public StringLineGroup Remaining() + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public char NextChar() + { + Start++; + if (Start <= End) { - StringLineGroup lines = _lines; - if (IsEmpty) + ref StringSlice slice = ref _currentSlice; + _offset++; + + int index = slice.Start + _offset; + string text = slice.Text; + if (index <= slice.End && (uint)index < (uint)text.Length) { - lines.Clear(); + char c = text[index]; + CurrentChar = c; + return c; } else { - lines.RemoveStartRange(SliceIndex); - - if (lines.Count > 0 && _offset > 0) - { - ref StringLine line = ref lines.Lines[0]; - line.Column += _offset; - line.Slice.Start += _offset; - } + return NextCharNewLine(); } - - return lines; } + else + { + return NextCharEndOfEnumerator(); + } + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public char NextChar() + private char NextCharNewLine() + { + int sliceLength = _currentSlice.Length; + NewLine newLine = _currentSlice.NewLine; + + if (_offset == sliceLength) { - Start++; - if (Start <= End) + if (newLine == NewLine.LineFeed) { - ref StringSlice slice = ref _currentSlice; - _offset++; - - int index = slice.Start + _offset; - string text = slice.Text; - if (index <= slice.End && (uint)index < (uint)text.Length) - { - char c = text[index]; - CurrentChar = c; - return c; - } - else - { - return NextCharNewLine(); - } + CurrentChar = '\n'; + goto MoveToNewLine; } - else + else if (newLine == NewLine.CarriageReturn) { - return NextCharEndOfEnumerator(); + CurrentChar = '\r'; + goto MoveToNewLine; } - } - - private char NextCharNewLine() - { - int sliceLength = _currentSlice.Length; - NewLine newLine = _currentSlice.NewLine; - - if (_offset == sliceLength) + else if (newLine == NewLine.CarriageReturnLineFeed) { - if (newLine == NewLine.LineFeed) - { - CurrentChar = '\n'; - goto MoveToNewLine; - } - else if (newLine == NewLine.CarriageReturn) - { - CurrentChar = '\r'; - goto MoveToNewLine; - } - else if (newLine == NewLine.CarriageReturnLineFeed) - { - CurrentChar = '\r'; - } + CurrentChar = '\r'; } - else if (_offset - 1 == sliceLength) + } + else if (_offset - 1 == sliceLength) + { + if (newLine == NewLine.CarriageReturnLineFeed) { - if (newLine == NewLine.CarriageReturnLineFeed) - { - CurrentChar = '\n'; - goto MoveToNewLine; - } + CurrentChar = '\n'; + goto MoveToNewLine; } + } - goto Return; + goto Return; - MoveToNewLine: - SliceIndex++; - _offset = -1; - _currentSlice = _lines.Lines[SliceIndex]; + MoveToNewLine: + SliceIndex++; + _offset = -1; + _currentSlice = _lines.Lines[SliceIndex]; - Return: - return CurrentChar; - } + Return: + return CurrentChar; + } - private char NextCharEndOfEnumerator() + 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); + + public readonly char PeekChar(int offset) + { + if (offset < 0) ThrowHelper.ArgumentOutOfRangeException("Negative offset are not supported for StringLineGroup", nameof(offset)); + + if (Start + offset > End) { - CurrentChar = '\0'; - Start = End + 1; - SliceIndex = _lines.Count; return '\0'; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SkipChar() => NextChar(); + offset += _offset; - public readonly char PeekChar() => PeekChar(1); + int sliceIndex = SliceIndex; + ref StringSlice slice = ref _lines.Lines[sliceIndex].Slice; + NewLine newLine = slice.NewLine; - public readonly char PeekChar(int offset) + if (!(newLine == NewLine.CarriageReturnLineFeed && offset == slice.Length + 1)) { - if (offset < 0) ThrowHelper.ArgumentOutOfRangeException("Negative offset are not supported for StringLineGroup", nameof(offset)); + while (offset > slice.Length) + { + // We are not peeking at the same line + offset -= slice.Length + 1; // + 1 for new line - if (Start + offset > End) + 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; + } + } + else + { + if (slice.NewLine == NewLine.CarriageReturnLineFeed) { - return '\0'; + return '\n'; // /n of /r/n (second character) } + } - offset += _offset; - - int sliceIndex = SliceIndex; - ref StringSlice slice = ref _lines.Lines[sliceIndex].Slice; - NewLine newLine = slice.NewLine; - - if (!(newLine == NewLine.CarriageReturnLineFeed && offset == slice.Length + 1)) + if (offset == slice.Length) + { + if (newLine == NewLine.LineFeed) { - 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.Count, "'Start + offset > End' check above should prevent us from indexing out of range"); - slice = ref _lines.Lines[++sliceIndex].Slice; - } + return '\n'; } - else + if (newLine == NewLine.CarriageReturn) { - if (slice.NewLine == NewLine.CarriageReturnLineFeed) - { - return '\n'; // /n of /r/n (second character) - } + return '\r'; } - - if (offset == slice.Length) + if (newLine == NewLine.CarriageReturnLineFeed) { - if (newLine == NewLine.LineFeed) - { - return '\n'; - } - if (newLine == NewLine.CarriageReturn) - { - return '\r'; - } - if (newLine == NewLine.CarriageReturnLineFeed) - { - return '\r'; // /r of /r/n (first character) - } + return '\r'; // /r of /r/n (first character) } - - Debug.Assert(offset < slice.Length); - return slice[slice.Start + offset]; } - public bool TrimStart() - { - var c = CurrentChar; - while (c.IsWhitespace()) - { - c = NextChar(); - } - return IsEmpty; - } + Debug.Assert(offset < slice.Length); + return slice[slice.Start + offset]; } - public readonly struct LineOffset + public bool TrimStart() { - public LineOffset(int linePosition, int column, int offset, int start, int end) + var c = CurrentChar; + while (c.IsWhitespace()) { - LinePosition = linePosition; - Column = column; - Offset = offset; - Start = start; - End = end; + c = NextChar(); } + return IsEmpty; + } + } - public readonly int LinePosition; + public readonly struct LineOffset + { + public LineOffset(int linePosition, int column, int offset, int start, int end) + { + LinePosition = linePosition; + Column = column; + Offset = offset; + Start = start; + End = end; + } - public readonly int Column; + public readonly int LinePosition; - public readonly int Offset; + public readonly int Column; - public readonly int Start; + public readonly int Offset; - public readonly int End; - } + public readonly int Start; + + public readonly int End; } } \ No newline at end of file diff --git a/src/Markdig/Helpers/StringSlice.cs b/src/Markdig/Helpers/StringSlice.cs index b812dcf4e..ad250053e 100644 --- a/src/Markdig/Helpers/StringSlice.cs +++ b/src/Markdig/Helpers/StringSlice.cs @@ -4,511 +4,509 @@ #nullable disable -using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -namespace Markdig.Helpers +namespace Markdig.Helpers; + +/// +/// A lightweight struct that represents a slice of a string. +/// +/// +public struct StringSlice : ICharIterator { /// - /// A lightweight struct that represents a slice of a string. + /// An empty string slice. + /// + public static readonly StringSlice Empty = new StringSlice(string.Empty); + + /// + /// Initializes a new instance of the struct. /// - /// - public struct StringSlice : ICharIterator + /// The text. + public StringSlice(string text) { - /// - /// An empty string slice. - /// - public static readonly StringSlice Empty = new StringSlice(string.Empty); - - /// - /// Initializes a new instance of the struct. - /// - /// The text. - public StringSlice(string text) - { - Text = text; - Start = 0; - End = (Text?.Length ?? 0) - 1; - NewLine = NewLine.None; - } + Text = text; + Start = 0; + End = (Text?.Length ?? 0) - 1; + NewLine = NewLine.None; + } - /// - /// Initializes a new instance of the struct. - /// - /// The text. - /// The line separation. - public StringSlice(string text, NewLine newLine) - { - Text = text; - Start = 0; - End = (Text?.Length ?? 0) - 1; - NewLine = newLine; - } + /// + /// Initializes a new instance of the struct. + /// + /// The text. + /// The line separation. + public StringSlice(string text, NewLine newLine) + { + Text = text; + Start = 0; + End = (Text?.Length ?? 0) - 1; + NewLine = newLine; + } - /// - /// Initializes a new instance of the struct. - /// - /// The text. - /// The start. - /// The end. - /// - public StringSlice(string text, int start, int end) - { - if (text is null) - ThrowHelper.ArgumentNullException_text(); + /// + /// Initializes a new instance of the struct. + /// + /// The text. + /// The start. + /// The end. + /// + public StringSlice(string text, int start, int end) + { + if (text is null) + ThrowHelper.ArgumentNullException_text(); - Text = text; - Start = start; - End = end; - NewLine = NewLine.None; - } + Text = text; + Start = start; + End = end; + NewLine = NewLine.None; + } - /// - /// Initializes a new instance of the struct. - /// - /// The text. - /// The start. - /// The end. - /// The line separation. - /// - public StringSlice(string text, int start, int end, NewLine newLine) - { - if (text is null) - ThrowHelper.ArgumentNullException_text(); + /// + /// Initializes a new instance of the struct. + /// + /// The text. + /// The start. + /// The end. + /// The line separation. + /// + public StringSlice(string text, int start, int end, NewLine newLine) + { + if (text is null) + ThrowHelper.ArgumentNullException_text(); - Text = text; - Start = start; - End = end; - NewLine = newLine; - } + Text = text; + Start = start; + End = end; + 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; - } + // 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; + } - /// - /// The text of this slice. - /// - public readonly string Text; + /// + /// The text of this slice. + /// + public readonly string Text; - /// - /// Gets or sets the start position within . - /// - public int Start { readonly get; set; } + /// + /// Gets or sets the start position within . + /// + public int Start { readonly get; set; } - /// - /// Gets or sets the end position (inclusive) within . - /// - public int End { readonly get; set; } + /// + /// Gets or sets the end position (inclusive) within . + /// + public int End { readonly get; set; } - /// - /// Gets the length. - /// - public readonly int Length => End - Start + 1; + /// + /// Gets the length. + /// + public readonly int Length => End - Start + 1; - public NewLine NewLine; + public NewLine NewLine; - /// - /// Gets the current character. - /// - public readonly char CurrentChar + /// + /// Gets the current character. + /// + public readonly char CurrentChar + { + get { - get - { - int start = Start; - return start <= End ? Text[start] : '\0'; - } + int start = Start; + return start <= End ? Text[start] : '\0'; } + } - /// - /// Gets a value indicating whether this instance is empty. - /// - public readonly bool IsEmpty - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => Start > End; - } + /// + /// Gets a value indicating whether this instance is empty. + /// + public readonly bool IsEmpty + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Start > End; + } - /// - /// Gets the at the specified index. - /// - /// The index. - /// A character in the slice at the specified index (not from but from the begining of the slice) - public readonly char this[int index] - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => Text[index]; - } + /// + /// Gets the at the specified index. + /// + /// The index. + /// A character in the slice at the specified index (not from but from the begining of the slice) + public readonly char this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Text[index]; + } - /// - /// Goes to the next character, incrementing the position. - /// - /// - /// The next character. `\0` is end of the iteration. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public char NextChar() + /// + /// Goes to the next character, incrementing the position. + /// + /// + /// The next character. `\0` is end of the iteration. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public char NextChar() + { + int start = Start; + if (start >= End) { - int start = Start; - if (start >= End) - { - Start = End + 1; - return '\0'; - } - start++; - Start = start; - return Text[start]; + Start = End + 1; + return '\0'; } + start++; + Start = start; + return Text[start]; + } - /// - /// Goes to the next character, incrementing the position. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SkipChar() + /// + /// Goes to the next character, incrementing the position. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SkipChar() + { + int start = Start; + if (start <= End) + Start = start + 1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal int CountAndSkipChar(char matchChar) + { + string text = Text; + int end = End; + int current = Start; + + while (current <= end && (uint)current < (uint)text.Length && text[current] == matchChar) { - int start = Start; - if (start <= End) - Start = start + 1; + current++; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal int CountAndSkipChar(char matchChar) - { - string text = Text; - int end = End; - int current = Start; + int count = current - Start; + Start = current; + return count; + } - while (current <= end && (uint)current < (uint)text.Length && text[current] == matchChar) - { - current++; - } + /// + /// Peeks a character at the offset of 1 from the current position + /// inside the range and , returns `\0` if outside this range. + /// + /// The character at offset, returns `\0` if none. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly char PeekChar() + { + int index = Start + 1; + return index <= End ? Text[index] : '\0'; + } - int count = current - Start; - Start = current; - return count; - } + /// + /// Peeks a character at the specified offset from the current position + /// inside the range and , returns `\0` if outside this range. + /// + /// The offset. + /// The character at offset, returns `\0` if none. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly char PeekChar(int offset) + { + var index = Start + offset; + return index >= Start && index <= End ? Text[index] : '\0'; + } - /// - /// Peeks a character at the offset of 1 from the current position - /// inside the range and , returns `\0` if outside this range. - /// - /// The character at offset, returns `\0` if none. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly char PeekChar() - { - int index = Start + 1; - return index <= End ? Text[index] : '\0'; - } + /// + /// Peeks a character at the specified offset from the current beginning of the string, without taking into account and + /// + /// The character at offset, returns `\0` if none. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly char PeekCharAbsolute(int index) + { + string text = Text; + return (uint)index < (uint)text.Length ? text[index] : '\0'; + } - /// - /// Peeks a character at the specified offset from the current position - /// inside the range and , returns `\0` if outside this range. - /// - /// The offset. - /// The character at offset, returns `\0` if none. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly char PeekChar(int offset) - { - var index = Start + offset; - return index >= Start && index <= End ? Text[index] : '\0'; - } + /// + /// Peeks a character at the specified offset from the current begining of the slice + /// without using the range or , returns `\0` if outside the . + /// + /// The offset. + /// The character at offset, returns `\0` if none. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly char PeekCharExtra(int offset) + { + var index = Start + offset; + var text = Text; + return (uint)index < (uint)text.Length ? text[index] : '\0'; + } - /// - /// Peeks a character at the specified offset from the current beginning of the string, without taking into account and - /// - /// The character at offset, returns `\0` if none. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly char PeekCharAbsolute(int index) - { - string text = Text; - return (uint)index < (uint)text.Length ? text[index] : '\0'; - } + /// + /// Matches the specified text. + /// + /// The text. + /// The offset. + /// true if the text matches; false otherwise + public readonly bool Match(string text, int offset = 0) + { + return Match(text, End, offset); + } - /// - /// Peeks a character at the specified offset from the current begining of the slice - /// without using the range or , returns `\0` if outside the . - /// - /// The offset. - /// The character at offset, returns `\0` if none. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly char PeekCharExtra(int offset) - { - var index = Start + offset; - var text = Text; - return (uint)index < (uint)text.Length ? text[index] : '\0'; - } + /// + /// Matches the specified text. + /// + /// The text. + /// The end. + /// The offset. + /// true if the text matches; false otherwise + public readonly bool Match(string text, int end, int offset) + { + var index = Start + offset; - /// - /// Matches the specified text. - /// - /// The text. - /// The offset. - /// true if the text matches; false otherwise - public readonly bool Match(string text, int offset = 0) - { - return Match(text, End, offset); - } + if (end - index + 1 < text.Length) + return false; - /// - /// Matches the specified text. - /// - /// The text. - /// The end. - /// The offset. - /// true if the text matches; false otherwise - public readonly bool Match(string text, int end, int offset) + string sliceText = Text; + for (int i = 0; i < text.Length; i++, index++) { - var index = Start + offset; - - if (end - index + 1 < text.Length) - return false; - - string sliceText = Text; - for (int i = 0; i < text.Length; i++, index++) + if (text[i] != sliceText[index]) { - if (text[i] != sliceText[index]) - { - return false; - } + return false; } - return true; } + return true; + } - /// - /// Expect spaces until a end of line. Return false otherwise. - /// - /// true if whitespaces where matched until a end of line - public bool SkipSpacesToEndOfLineOrEndOfDocument() + /// + /// Expect spaces until a end of line. Return false otherwise. + /// + /// true if whitespaces where matched until a end of line + public bool SkipSpacesToEndOfLineOrEndOfDocument() + { + for (int i = Start; i <= End; i++) { - for (int i = Start; i <= End; i++) + var c = Text[i]; + if (c.IsWhitespace()) { - var c = Text[i]; - if (c.IsWhitespace()) + if (c == '\0' || c == '\n' || (c == '\r' && i + 1 <= End && Text[i + 1] != '\n')) { - if (c == '\0' || c == '\n' || (c == '\r' && i + 1 <= End && Text[i + 1] != '\n')) - { - return true; - } - continue; + return true; } - return false; + continue; } - return true; + return false; } + return true; + } - /// - /// Matches the specified text using lowercase comparison. - /// - /// The text. - /// The offset. - /// true if the text matches; false otherwise - public readonly bool MatchLowercase(string text, int offset = 0) - { - return MatchLowercase(text, End, offset); - } + /// + /// Matches the specified text using lowercase comparison. + /// + /// The text. + /// The offset. + /// true if the text matches; false otherwise + public readonly bool MatchLowercase(string text, int offset = 0) + { + return MatchLowercase(text, End, offset); + } - /// - /// Matches the specified text using lowercase comparison. - /// - /// The text. - /// The end. - /// The offset. - /// true if the text matches; false otherwise - public readonly bool MatchLowercase(string text, int end, int offset) - { - var index = Start + offset; + /// + /// Matches the specified text using lowercase comparison. + /// + /// The text. + /// The end. + /// The offset. + /// true if the text matches; false otherwise + public readonly bool MatchLowercase(string text, int end, int offset) + { + var index = Start + offset; - if (end - index + 1 < text.Length) - return false; + if (end - index + 1 < text.Length) + return false; - string sliceText = Text; - for (int i = 0; i < text.Length; i++, index++) + string sliceText = Text; + for (int i = 0; i < text.Length; i++, index++) + { + if (text[i] != char.ToLowerInvariant(sliceText[index])) { - if (text[i] != char.ToLowerInvariant(sliceText[index])) - { - return false; - } + return false; } - return true; } + return true; + } - /// - /// Searches the specified text within this slice. - /// - /// The text. - /// The offset. - /// true if ignore case - /// true if the text was found; false otherwise - public readonly int IndexOf(string text, int offset = 0, bool ignoreCase = false) - { - offset += Start; - int length = End - offset + 1; + /// + /// Searches the specified text within this slice. + /// + /// The text. + /// The offset. + /// true if ignore case + /// true if the text was found; false otherwise + public readonly int IndexOf(string text, int offset = 0, bool ignoreCase = false) + { + offset += Start; + int length = End - offset + 1; - if (length <= 0) - return -1; + if (length <= 0) + return -1; - return Text.IndexOf(text, offset, length, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); - } + return Text.IndexOf(text, offset, length, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); + } - /// - /// Searches for the specified character within this slice. - /// - /// A value >= 0 if the character was found, otherwise < 0 - public readonly int IndexOf(char c) - { - int start = Start; - int length = End - start + 1; + /// + /// Searches for the specified character within this slice. + /// + /// A value >= 0 if the character was found, otherwise < 0 + public readonly int IndexOf(char c) + { + int start = Start; + int length = End - start + 1; - if (length <= 0) - return -1; + if (length <= 0) + return -1; - return Text.IndexOf(c, start, length); - } + return Text.IndexOf(c, start, length); + } - /// - /// Trims whitespaces at the beginning of this slice starting from position. - /// - /// - /// true if it has reaches the end of the iterator - /// - public bool TrimStart() - { - string text = Text; - int end = End; - int i = Start; + /// + /// Trims whitespaces at the beginning of this slice starting from position. + /// + /// + /// true if it has reaches the end of the iterator + /// + public bool TrimStart() + { + string text = Text; + int end = End; + int i = Start; - while (i <= end && (uint)i < (uint)text.Length && text[i].IsWhitespace()) - i++; + while (i <= end && (uint)i < (uint)text.Length && text[i].IsWhitespace()) + i++; - Start = i; - return i > end; - } + Start = i; + return i > end; + } - /// - /// Trims whitespaces at the beginning of this slice starting from position. - /// - /// The number of spaces trimmed. - public void TrimStart(out int spaceCount) - { - string text = Text; - int end = End; - int i = Start; + /// + /// Trims whitespaces at the beginning of this slice starting from position. + /// + /// The number of spaces trimmed. + public void TrimStart(out int spaceCount) + { + string text = Text; + int end = End; + int i = Start; - while (i <= end && (uint)i < (uint)text.Length && text[i].IsWhitespace()) - i++; + while (i <= end && (uint)i < (uint)text.Length && text[i].IsWhitespace()) + i++; - spaceCount = i - Start; - Start = i; - } + spaceCount = i - Start; + Start = i; + } - /// - /// Trims whitespaces at the end of this slice, starting from position. - /// - /// - public bool TrimEnd() - { - string text = Text; - int start = Start; - int i = End; + /// + /// Trims whitespaces at the end of this slice, starting from position. + /// + /// + public bool TrimEnd() + { + string text = Text; + int start = Start; + int i = End; - while (start <= i && (uint)i < (uint)text.Length && text[i].IsWhitespace()) - i--; + while (start <= i && (uint)i < (uint)text.Length && text[i].IsWhitespace()) + i--; - End = i; - return start > i; - } + End = i; + return start > i; + } - /// - /// Trims whitespaces from both the start and end of this slice. - /// - public void Trim() - { - string text = Text; - int start = Start; - int end = End; + /// + /// Trims whitespaces from both the start and end of this slice. + /// + public void Trim() + { + string text = Text; + int start = Start; + int end = End; - while (start <= end && (uint)start < (uint)text.Length && text[start].IsWhitespace()) - start++; + while (start <= end && (uint)start < (uint)text.Length && text[start].IsWhitespace()) + start++; - while (start <= end && (uint)end < (uint)text.Length && text[end].IsWhitespace()) - end--; + while (start <= end && (uint)end < (uint)text.Length && text[end].IsWhitespace()) + end--; - Start = start; - End = end; - } + Start = start; + End = end; + } - /// - /// Returns a that represents this instance. - /// - /// - /// A that represents this instance. - /// - public readonly override string ToString() - { - string text = Text; - int start = Start; - int length = End - start + 1; + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public readonly override string ToString() + { + string text = Text; + int start = Start; + int length = End - start + 1; - if (text is null || length <= 0) - { - return string.Empty; - } - return text.Substring(start, length); + if (text is null || length <= 0) + { + return string.Empty; } + return text.Substring(start, length); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly ReadOnlySpan AsSpan() - { - string text = Text; - int start = Start; - int length = End - start + 1; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly ReadOnlySpan 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 (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); + return MemoryMarshal.CreateReadOnlySpan(ref Unsafe.Add(ref Unsafe.AsRef(text.GetPinnableReference()), start), length); #else - return text.AsSpan(start, length); + return text.AsSpan(start, length); #endif - } - - /// - /// Determines whether this slice is empty or made only of whitespaces. - /// - /// true if this slice is empty or made only of whitespaces; false otherwise - public readonly bool IsEmptyOrWhitespace() - { - string text = Text; - int end = End; + } - for (int i = Start; i <= end && (uint)i < (uint)text.Length; i++) - { - if (!text[i].IsWhitespace()) - { - return false; - } - } - return true; - } + /// + /// Determines whether this slice is empty or made only of whitespaces. + /// + /// true if this slice is empty or made only of whitespaces; false otherwise + public readonly bool IsEmptyOrWhitespace() + { + string text = Text; + int end = End; - public bool Overlaps(StringSlice other) + for (int i = Start; i <= end && (uint)i < (uint)text.Length; i++) { - if (IsEmpty || other.IsEmpty) + if (!text[i].IsWhitespace()) { return false; } + } + return true; + } - return Start <= other.End && End >= other.Start; + public bool Overlaps(StringSlice other) + { + if (IsEmpty || other.IsEmpty) + { + return false; } + + return Start <= other.End && End >= other.Start; } } diff --git a/src/Markdig/Helpers/ThrowHelper.cs b/src/Markdig/Helpers/ThrowHelper.cs index 0e088bc4a..4253a5044 100644 --- a/src/Markdig/Helpers/ThrowHelper.cs +++ b/src/Markdig/Helpers/ThrowHelper.cs @@ -2,151 +2,149 @@ // 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.Runtime.CompilerServices; -namespace Markdig.Helpers +namespace Markdig.Helpers; + +/// +/// Inspired by CoreLib, taken from https://github.com/MihaZupan/SharpCollections, cc @MihaZupan +/// +[ExcludeFromCodeCoverage] +internal static class ThrowHelper { - /// - /// Inspired by CoreLib, taken from https://github.com/MihaZupan/SharpCollections, cc @MihaZupan - /// - [ExcludeFromCodeCoverage] - internal static class ThrowHelper - { - [DoesNotReturn] - public static void ArgumentNullException(string paramName) => throw new ArgumentNullException(paramName); + [DoesNotReturn] + public static void ArgumentNullException(string paramName) => throw new ArgumentNullException(paramName); - [DoesNotReturn] - public static void ArgumentNullException_item() => throw new ArgumentNullException("item"); + [DoesNotReturn] + public static void ArgumentNullException_item() => throw new ArgumentNullException("item"); - [DoesNotReturn] - public static void ArgumentNullException_text() => throw new ArgumentNullException("text"); + [DoesNotReturn] + public static void ArgumentNullException_text() => throw new ArgumentNullException("text"); - [DoesNotReturn] - public static void ArgumentNullException_label() => throw new ArgumentNullException("label"); + [DoesNotReturn] + public static void ArgumentNullException_label() => throw new ArgumentNullException("label"); - [DoesNotReturn] - public static void ArgumentNullException_key() => throw new ArgumentNullException("key"); + [DoesNotReturn] + public static void ArgumentNullException_key() => throw new ArgumentNullException("key"); - [DoesNotReturn] - public static void ArgumentNullException_name() => throw new ArgumentNullException("name"); + [DoesNotReturn] + public static void ArgumentNullException_name() => throw new ArgumentNullException("name"); - [DoesNotReturn] - public static void ArgumentNullException_markdown() => throw new ArgumentNullException("markdown"); + [DoesNotReturn] + public static void ArgumentNullException_markdown() => throw new ArgumentNullException("markdown"); - [DoesNotReturn] - public static void ArgumentNullException_writer() => throw new ArgumentNullException("writer"); + [DoesNotReturn] + public static void ArgumentNullException_writer() => throw new ArgumentNullException("writer"); - [DoesNotReturn] - public static void ArgumentNullException_leafBlock() => throw new ArgumentNullException("leafBlock"); + [DoesNotReturn] + public static void ArgumentNullException_leafBlock() => throw new ArgumentNullException("leafBlock"); - [DoesNotReturn] - public static void ArgumentNullException_markdownObject() => throw new ArgumentNullException("markdownObject"); + [DoesNotReturn] + public static void ArgumentNullException_markdownObject() => throw new ArgumentNullException("markdownObject"); - [DoesNotReturn] - public static void ArgumentException(string message) => throw new ArgumentException(message); + [DoesNotReturn] + public static void ArgumentException(string message) => throw new ArgumentException(message); - [DoesNotReturn] - public static void ArgumentException(string message, string paramName) => throw new ArgumentException(message, paramName); + [DoesNotReturn] + public static void ArgumentException(string message, string paramName) => throw new ArgumentException(message, paramName); - [DoesNotReturn] - public static void ArgumentOutOfRangeException(string paramName) => throw new ArgumentOutOfRangeException(paramName); + [DoesNotReturn] + public static void ArgumentOutOfRangeException(string paramName) => throw new ArgumentOutOfRangeException(paramName); - [DoesNotReturn] - public static void ArgumentOutOfRangeException(string message, string paramName) => throw new ArgumentOutOfRangeException(paramName, message); + [DoesNotReturn] + public static void ArgumentOutOfRangeException(string message, string paramName) => throw new ArgumentOutOfRangeException(paramName, message); - [DoesNotReturn] - public static void ArgumentOutOfRangeException_index() => throw new ArgumentOutOfRangeException("index"); + [DoesNotReturn] + public static void ArgumentOutOfRangeException_index() => throw new ArgumentOutOfRangeException("index"); - [DoesNotReturn] - public static void InvalidOperationException(string message) => throw new InvalidOperationException(message); + [DoesNotReturn] + public static void InvalidOperationException(string message) => throw new InvalidOperationException(message); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void CheckDepthLimit(int depth, bool useLargeLimit = false) - { - // Very conservative limit used to limit nesting in the final AST - // Used to avoid a StackOverflow in the recursive rendering process - const int DepthLimit = 128; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void CheckDepthLimit(int depth, bool useLargeLimit = false) + { + // Very conservative limit used to limit nesting in the final AST + // Used to avoid a StackOverflow in the recursive rendering process + const int DepthLimit = 128; - // Limit used for reducing the maximum execution time for pathological-case inputs - // Applies to: - // a) inputs that would fail depth checks in the future (for example "[[[[[..." or ">>>>>>...") - // b) very large pipe tables - const int LargeDepthLimit = 10 * 1024; + // Limit used for reducing the maximum execution time for pathological-case inputs + // Applies to: + // a) inputs that would fail depth checks in the future (for example "[[[[[..." or ">>>>>>...") + // b) very large pipe tables + const int LargeDepthLimit = 10 * 1024; - int limit = useLargeLimit ? LargeDepthLimit : DepthLimit; + int limit = useLargeLimit ? LargeDepthLimit : DepthLimit; - if (depth > limit) - DepthLimitExceeded(); + if (depth > limit) + DepthLimitExceeded(); - [MethodImpl(MethodImplOptions.NoInlining)] - static void DepthLimitExceeded() => throw new ArgumentException("Markdown elements in the input are too deeply nested - depth limit exceeded. Input is most likely not sensible or is a very large table."); - } + [MethodImpl(MethodImplOptions.NoInlining)] + static void DepthLimitExceeded() => throw new ArgumentException("Markdown elements in the input are too deeply nested - depth limit exceeded. Input is most likely not sensible or is a very large table."); + } - [DoesNotReturn] - public static void ThrowArgumentNullException(ExceptionArgument argument) - { - throw new ArgumentNullException(argument.ToString()); - } + [DoesNotReturn] + public static void ThrowArgumentNullException(ExceptionArgument argument) + { + throw new ArgumentNullException(argument.ToString()); + } - [DoesNotReturn] - public static void ThrowArgumentException(ExceptionArgument argument, ExceptionReason reason) - { - throw new ArgumentException(argument.ToString(), GetExceptionReason(reason)); - } + [DoesNotReturn] + public static void ThrowArgumentException(ExceptionArgument argument, ExceptionReason reason) + { + throw new ArgumentException(argument.ToString(), GetExceptionReason(reason)); + } - [DoesNotReturn] - public static void ThrowArgumentOutOfRangeException(ExceptionArgument argument, ExceptionReason reason) - { - throw new ArgumentOutOfRangeException(argument.ToString(), GetExceptionReason(reason)); - } + [DoesNotReturn] + public static void ThrowArgumentOutOfRangeException(ExceptionArgument argument, ExceptionReason reason) + { + throw new ArgumentOutOfRangeException(argument.ToString(), GetExceptionReason(reason)); + } - [DoesNotReturn] - public static void ThrowIndexOutOfRangeException() - { - throw new IndexOutOfRangeException(); - } + [DoesNotReturn] + public static void ThrowIndexOutOfRangeException() + { + throw new IndexOutOfRangeException(); + } - private static string GetExceptionReason(ExceptionReason reason) + private static string GetExceptionReason(ExceptionReason reason) + { + switch (reason) { - switch (reason) - { - case ExceptionReason.String_Empty: - return "String must not be empty."; + case ExceptionReason.String_Empty: + return "String must not be empty."; - case ExceptionReason.SmallCapacity: - return "Capacity was less than the current size."; + case ExceptionReason.SmallCapacity: + return "Capacity was less than the current size."; - case ExceptionReason.InvalidOffsetLength: - return "Offset and length must refer to a position in the string."; + case ExceptionReason.InvalidOffsetLength: + return "Offset and length must refer to a position in the string."; - case ExceptionReason.DuplicateKey: - return "The given key is already present in the dictionary."; + case ExceptionReason.DuplicateKey: + return "The given key is already present in the dictionary."; - default: - Debug.Assert(false, "The enum value is not defined, please check the ExceptionReason Enum."); - return ""; - } + default: + Debug.Assert(false, "The enum value is not defined, please check the ExceptionReason Enum."); + return ""; } } +} - internal enum ExceptionArgument - { - key, - input, - value, - length, - offsetLength, - text - } +internal enum ExceptionArgument +{ + key, + input, + value, + length, + offsetLength, + text +} - internal enum ExceptionReason - { - String_Empty, - SmallCapacity, - InvalidOffsetLength, - DuplicateKey, - } +internal enum ExceptionReason +{ + String_Empty, + SmallCapacity, + InvalidOffsetLength, + DuplicateKey, } diff --git a/src/Markdig/Helpers/TransformedStringCache.cs b/src/Markdig/Helpers/TransformedStringCache.cs index dadbbd025..16f442077 100644 --- a/src/Markdig/Helpers/TransformedStringCache.cs +++ b/src/Markdig/Helpers/TransformedStringCache.cs @@ -2,125 +2,123 @@ // 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 +namespace Markdig.Helpers; + +internal sealed class TransformedStringCache { - internal sealed class TransformedStringCache - { - internal const int InputLengthLimit = 20; // Avoid caching unreasonably long strings - internal const int MaxEntriesPerCharacter = 8; // Avoid growing too much + 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 _transformation; - private readonly EntryGroup[] _groups; // One per ASCII character - private readonly Func _transformation; + public TransformedStringCache(Func transformation) + { + _transformation = transformation ?? throw new ArgumentNullException(nameof(transformation)); + _groups = new EntryGroup[128]; + } - public TransformedStringCache(Func transformation) + public string Get(ReadOnlySpan inputSpan) + { + if ((uint)(inputSpan.Length - 1) < InputLengthLimit) // Length: [1, LengthLimit] { - _transformation = transformation ?? throw new ArgumentNullException(nameof(transformation)); - _groups = new EntryGroup[128]; + 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; + } } - public string Get(ReadOnlySpan inputSpan) + return _transformation(inputSpan.ToString()); + } + + public string Get(string input) + { + if ((uint)(input.Length - 1) < InputLengthLimit) // Length: [1, LengthLimit] { - if ((uint)(inputSpan.Length - 1) < InputLengthLimit) // Length: [1, LengthLimit] + int firstCharacter = input[0]; + EntryGroup[] groups = _groups; + if ((uint)firstCharacter < (uint)groups.Length) { - int firstCharacter = inputSpan[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) { - 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; + transformed = _transformation(input); + group.TryAdd(input, transformed); } + return transformed; } + } - return _transformation(inputSpan.ToString()); + return _transformation(input); + } + + private struct EntryGroup + { + private struct Entry + { + public string Input; + public string Transformed; } - public string Get(string input) + private Entry[]? _entries; + + public string? TryGet(ReadOnlySpan inputSpan) { - if ((uint)(input.Length - 1) < InputLengthLimit) // Length: [1, LengthLimit] + Entry[]? entries = _entries; + if (entries is not null) { - int firstCharacter = input[0]; - EntryGroup[] groups = _groups; - if ((uint)firstCharacter < (uint)groups.Length) + for (int i = 0; i < entries.Length; i++) { - ref EntryGroup group = ref groups[firstCharacter]; - string? transformed = group.TryGet(input.AsSpan()); - if (transformed is null) + if (inputSpan.SequenceEqual(entries[i].Input.AsSpan())) { - transformed = _transformation(input); - group.TryAdd(input, transformed); + return entries[i].Transformed; } - return transformed; } } - - return _transformation(input); + return null; } - private struct EntryGroup + public void TryAdd(string input, string transformed) { - private struct Entry + if (_entries is null) { - public string Input; - public string Transformed; + Interlocked.CompareExchange(ref _entries, new Entry[MaxEntriesPerCharacter], null); } - private Entry[]? _entries; - - public string? TryGet(ReadOnlySpan inputSpan) + if (_entries[MaxEntriesPerCharacter - 1].Input is null) // There is still space { - Entry[]? entries = _entries; - if (entries is not null) + lock (_entries) { - for (int i = 0; i < entries.Length; i++) + for (int i = 0; i < _entries.Length; i++) { - if (inputSpan.SequenceEqual(entries[i].Input.AsSpan())) + string? existingInput = _entries[i].Input; + + if (existingInput is null) { - return entries[i].Transformed; + ref Entry entry = ref _entries[i]; + Volatile.Write(ref entry.Transformed, transformed); + Volatile.Write(ref entry.Input, input); + break; } - } - } - 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++) + if (input == existingInput) { - 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; - } + // We lost a race and a different thread already added the same value + break; } } } diff --git a/src/Markdig/Helpers/ValueStringBuilder.cs b/src/Markdig/Helpers/ValueStringBuilder.cs index 5cdcb2591..17f37bb6e 100644 --- a/src/Markdig/Helpers/ValueStringBuilder.cs +++ b/src/Markdig/Helpers/ValueStringBuilder.cs @@ -4,194 +4,192 @@ // 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 +namespace Markdig.Helpers; + +internal ref partial struct ValueStringBuilder { - internal ref partial struct ValueStringBuilder - { #if DEBUG - public const int StackallocThreshold = 7; + public const int StackallocThreshold = 7; #else #if NET5_0_OR_GREATER - // NET5+ has SkipLocalsInit, so allocating more is "free" - public const int StackallocThreshold = 256; + // NET5+ has SkipLocalsInit, so allocating more is "free" + public const int StackallocThreshold = 256; #else - public const int StackallocThreshold = 64; + public const int StackallocThreshold = 64; #endif #endif - private char[]? _arrayToReturnToPool; - private Span _chars; - private int _pos; + private char[]? _arrayToReturnToPool; + private Span _chars; + private int _pos; + + public ValueStringBuilder(Span initialBuffer) + { + _arrayToReturnToPool = null; + _chars = initialBuffer; + _pos = 0; + } - public ValueStringBuilder(Span initialBuffer) + public int Length + { + get => _pos; + set { - _arrayToReturnToPool = null; - _chars = initialBuffer; - _pos = 0; + Debug.Assert(value >= 0); + Debug.Assert(value <= _chars.Length); + _pos = value; } + } - public int Length + public ref char this[int index] + { + get { - get => _pos; - set - { - Debug.Assert(value >= 0); - Debug.Assert(value <= _chars.Length); - _pos = value; - } + Debug.Assert(index < _pos); + return ref _chars[index]; } + } + + public override string ToString() + { + string s = _chars.Slice(0, _pos).ToString(); + Dispose(); + return s; + } + + public ReadOnlySpan AsSpan() => _chars.Slice(0, _pos); - public ref char this[int index] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(char c) + { + int pos = _pos; + Span chars = _chars; + if ((uint)pos < (uint)chars.Length) { - get - { - Debug.Assert(index < _pos); - return ref _chars[index]; - } + chars[pos] = c; + _pos = pos + 1; } - - public override string ToString() + else { - string s = _chars.Slice(0, _pos).ToString(); - Dispose(); - return s; + GrowAndAppend(c); } + } - public ReadOnlySpan AsSpan() => _chars.Slice(0, _pos); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Append(char c) + public void Append(char c, int count) + { + if (_pos > _chars.Length - count) { - int pos = _pos; - Span chars = _chars; - if ((uint)pos < (uint)chars.Length) - { - chars[pos] = c; - _pos = pos + 1; - } - else - { - GrowAndAppend(c); - } + Grow(count); } - public void Append(char c, int count) + Span dst = _chars.Slice(_pos, count); + for (int i = 0; i < dst.Length; i++) { - if (_pos > _chars.Length - count) - { - Grow(count); - } - - Span dst = _chars.Slice(_pos, count); - for (int i = 0; i < dst.Length; i++) - { - dst[i] = c; - } - _pos += count; + dst[i] = c; } + _pos += count; + } - public void Append(uint i) + public void Append(uint i) + { + if (i < 10) + { + Append((char)('0' + i)); + } + else { - if (i < 10) - { - Append((char)('0' + i)); - } - else - { - Append(i.ToString()); - } + Append(i.ToString()); } + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Append(string s) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(string s) + { + int pos = _pos; + if (pos > _chars.Length - s.Length) { - int pos = _pos; - if (pos > _chars.Length - s.Length) - { - Grow(s.Length); - } + Grow(s.Length); + } - s + s #if !NET5_0_OR_GREATER - .AsSpan() + .AsSpan() #endif - .CopyTo(_chars.Slice(pos)); + .CopyTo(_chars.Slice(pos)); - _pos += s.Length; - } + _pos += s.Length; + } - public void Append(ReadOnlySpan value) + public void Append(ReadOnlySpan value) + { + if (_pos > _chars.Length - value.Length) { - if (_pos > _chars.Length - value.Length) - { - Grow(value.Length); - } - - value.CopyTo(_chars.Slice(_pos)); - _pos += value.Length; + Grow(value.Length); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Span AppendSpan(int length) - { - int origPos = _pos; - if (origPos > _chars.Length - length) - { - Grow(length); - } - - _pos = origPos + length; - return _chars.Slice(origPos, length); - } + value.CopyTo(_chars.Slice(_pos)); + _pos += value.Length; + } - [MethodImpl(MethodImplOptions.NoInlining)] - private void GrowAndAppend(char c) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span AppendSpan(int length) + { + int origPos = _pos; + if (origPos > _chars.Length - length) { - Grow(1); - Append(c); + Grow(length); } - /// - /// Resize the internal buffer either by doubling current buffer size or - /// by adding to - /// whichever is greater. - /// - /// - /// Number of chars requested beyond current position. - /// - [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."); + _pos = origPos + length; + return _chars.Slice(origPos, length); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void GrowAndAppend(char c) + { + Grow(1); + Append(c); + } - // Make sure to let Rent throw an exception if the caller has a bug and the desired capacity is negative - char[] poolArray = ArrayPool.Shared.Rent((int)Math.Max((uint)(_pos + additionalCapacityBeyondPos), (uint)_chars.Length * 2)); + /// + /// Resize the internal buffer either by doubling current buffer size or + /// by adding to + /// whichever is greater. + /// + /// + /// Number of chars requested beyond current position. + /// + [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.Shared.Rent((int)Math.Max((uint)(_pos + additionalCapacityBeyondPos), (uint)_chars.Length * 2)); - _chars.Slice(0, _pos).CopyTo(poolArray); + _chars.Slice(0, _pos).CopyTo(poolArray); - char[]? toReturn = _arrayToReturnToPool; - _chars = _arrayToReturnToPool = poolArray; - if (toReturn != null) - { - ArrayPool.Shared.Return(toReturn); - } + char[]? toReturn = _arrayToReturnToPool; + _chars = _arrayToReturnToPool = poolArray; + if (toReturn != null) + { + ArrayPool.Shared.Return(toReturn); } + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Dispose() + [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) { - char[]? toReturn = _arrayToReturnToPool; - this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again - if (toReturn != null) - { - ArrayPool.Shared.Return(toReturn); - } + ArrayPool.Shared.Return(toReturn); } } } diff --git a/src/Markdig/IMarkdownExtension.cs b/src/Markdig/IMarkdownExtension.cs index 6c404adb2..0a9ece31b 100644 --- a/src/Markdig/IMarkdownExtension.cs +++ b/src/Markdig/IMarkdownExtension.cs @@ -1,27 +1,26 @@ -// Copyright (c) Alexandre Mutel. All rights reserved. +// 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; -namespace Markdig +namespace Markdig; + +/// +/// Base interface for an extension. +/// +public interface IMarkdownExtension { /// - /// Base interface for an extension. + /// Setups this extension for the specified pipeline. /// - public interface IMarkdownExtension - { - /// - /// Setups this extension for the specified pipeline. - /// - /// The pipeline. - void Setup(MarkdownPipelineBuilder pipeline); + /// The pipeline. + void Setup(MarkdownPipelineBuilder pipeline); - /// - /// Setups this extension for the specified renderer. - /// - /// The pipeline used to parse the document. - /// The renderer. - void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer); - } + /// + /// Setups this extension for the specified renderer. + /// + /// The pipeline used to parse the document. + /// The renderer. + void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer); } \ No newline at end of file diff --git a/src/Markdig/Markdown.cs b/src/Markdig/Markdown.cs index 3bded907d..98670f3f5 100644 --- a/src/Markdig/Markdown.cs +++ b/src/Markdig/Markdown.cs @@ -2,285 +2,284 @@ // 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.IO; using System.Linq; using System.Reflection; + using Markdig.Helpers; using Markdig.Parsers; using Markdig.Renderers; using Markdig.Renderers.Normalize; using Markdig.Syntax; -namespace Markdig +namespace Markdig; + +/// +/// Provides methods for parsing a Markdown string to a syntax tree and converting it to other formats. +/// +public static partial class Markdown { - /// - /// Provides methods for parsing a Markdown string to a syntax tree and converting it to other formats. - /// - public static partial class Markdown + public static string Version { - public static string Version + get { - get - { - if (_Version == null) - _Version = ((AssemblyFileVersionAttribute)typeof(Markdown).Assembly.GetCustomAttributes(typeof(AssemblyFileVersionAttribute), false).FirstOrDefault())?.Version ?? "Unknown"; - return _Version; - } + if (_Version == null) + _Version = ((AssemblyFileVersionAttribute)typeof(Markdown).Assembly.GetCustomAttributes(typeof(AssemblyFileVersionAttribute), false).FirstOrDefault())?.Version ?? "Unknown"; + return _Version; } - private static string? _Version; + } + private static string? _Version; - internal static readonly MarkdownPipeline DefaultPipeline = new MarkdownPipelineBuilder().Build(); - private static readonly MarkdownPipeline _defaultTrackTriviaPipeline = new MarkdownPipelineBuilder().EnableTrackTrivia().Build(); + internal static readonly MarkdownPipeline DefaultPipeline = new MarkdownPipelineBuilder().Build(); + private static readonly MarkdownPipeline _defaultTrackTriviaPipeline = new MarkdownPipelineBuilder().EnableTrackTrivia().Build(); - private static MarkdownPipeline GetPipeline(MarkdownPipeline? pipeline, string markdown) + private static MarkdownPipeline GetPipeline(MarkdownPipeline? pipeline, string markdown) + { + if (pipeline is null) { - if (pipeline is null) - { - return DefaultPipeline; - } - - if (pipeline.SelfPipeline is not null) - { - return pipeline.SelfPipeline.CreatePipelineFromInput(markdown); - } - - return pipeline; + return DefaultPipeline; } - - /// - /// Normalizes the specified markdown to a normalized markdown text. - /// - /// The markdown. - /// The normalize options - /// The pipeline. - /// A parser context used for the parsing. - /// A normalized markdown text. - public static string Normalize(string markdown, NormalizeOptions? options = null, MarkdownPipeline? pipeline = null, MarkdownParserContext? context = null) + if (pipeline.SelfPipeline is not null) { - var writer = new StringWriter(); - Normalize(markdown, writer, options, pipeline, context); - return writer.ToString(); + return pipeline.SelfPipeline.CreatePipelineFromInput(markdown); } - /// - /// Normalizes the specified markdown to a normalized markdown text. - /// - /// The markdown. - /// The destination that will receive the result of the conversion. - /// The normalize options - /// The pipeline. - /// A parser context used for the parsing. - /// A normalized markdown text. - public static MarkdownDocument Normalize(string markdown, TextWriter writer, NormalizeOptions? options = null, MarkdownPipeline? pipeline = null, MarkdownParserContext? context = null) - { - if (markdown is null) ThrowHelper.ArgumentNullException_markdown(); + return pipeline; + } - pipeline = GetPipeline(pipeline, markdown); - var document = MarkdownParser.Parse(markdown, pipeline, context); + /// + /// Normalizes the specified markdown to a normalized markdown text. + /// + /// The markdown. + /// The normalize options + /// The pipeline. + /// A parser context used for the parsing. + /// A normalized markdown text. + public static string Normalize(string markdown, NormalizeOptions? options = null, MarkdownPipeline? pipeline = null, MarkdownParserContext? context = null) + { + var writer = new StringWriter(); + Normalize(markdown, writer, options, pipeline, context); + return writer.ToString(); + } - var renderer = new NormalizeRenderer(writer, options); - pipeline.Setup(renderer); + /// + /// Normalizes the specified markdown to a normalized markdown text. + /// + /// The markdown. + /// The destination that will receive the result of the conversion. + /// The normalize options + /// The pipeline. + /// A parser context used for the parsing. + /// A normalized markdown text. + public static MarkdownDocument Normalize(string markdown, TextWriter writer, NormalizeOptions? options = null, MarkdownPipeline? pipeline = null, MarkdownParserContext? context = null) + { + if (markdown is null) ThrowHelper.ArgumentNullException_markdown(); - renderer.Render(document); - writer.Flush(); + pipeline = GetPipeline(pipeline, markdown); - return document; - } + var document = MarkdownParser.Parse(markdown, pipeline, context); - /// - /// Converts a Markdown string to HTML. - /// - /// A Markdown text. - /// The pipeline used for the conversion. - /// A parser context used for the parsing. - /// The result of the conversion - /// if markdown variable is null - public static string ToHtml(string markdown, MarkdownPipeline? pipeline = null, MarkdownParserContext? context = null) - { - if (markdown is null) ThrowHelper.ArgumentNullException_markdown(); + var renderer = new NormalizeRenderer(writer, options); + pipeline.Setup(renderer); - pipeline = GetPipeline(pipeline, markdown); + renderer.Render(document); + writer.Flush(); - var document = MarkdownParser.Parse(markdown, pipeline, context); + return document; + } - return ToHtml(document, pipeline); - } + /// + /// Converts a Markdown string to HTML. + /// + /// A Markdown text. + /// The pipeline used for the conversion. + /// A parser context used for the parsing. + /// The result of the conversion + /// if markdown variable is null + public static string ToHtml(string markdown, MarkdownPipeline? pipeline = null, MarkdownParserContext? context = null) + { + if (markdown is null) ThrowHelper.ArgumentNullException_markdown(); - /// - /// Converts a Markdown document to HTML. - /// - /// A Markdown document. - /// The pipeline used for the conversion. - /// The result of the conversion - /// if markdown document variable is null - public static string ToHtml(this MarkdownDocument document, MarkdownPipeline? pipeline = null) - { - if (document is null) ThrowHelper.ArgumentNullException(nameof(document)); + pipeline = GetPipeline(pipeline, markdown); - pipeline ??= DefaultPipeline; + var document = MarkdownParser.Parse(markdown, pipeline, context); - using var rentedRenderer = pipeline.RentHtmlRenderer(); - HtmlRenderer renderer = rentedRenderer.Instance; + return ToHtml(document, pipeline); + } - renderer.Render(document); - renderer.Writer.Flush(); + /// + /// Converts a Markdown document to HTML. + /// + /// A Markdown document. + /// The pipeline used for the conversion. + /// The result of the conversion + /// if markdown document variable is null + public static string ToHtml(this MarkdownDocument document, MarkdownPipeline? pipeline = null) + { + if (document is null) ThrowHelper.ArgumentNullException(nameof(document)); - return renderer.Writer.ToString() ?? string.Empty; - } + pipeline ??= DefaultPipeline; - /// - /// Converts a Markdown document to HTML. - /// - /// A Markdown document. - /// The destination that will receive the result of the conversion. - /// The pipeline used for the conversion. - /// The result of the conversion - /// if markdown document variable is null - public static void ToHtml(this MarkdownDocument document, TextWriter writer, MarkdownPipeline? pipeline = null) - { - if (document is null) ThrowHelper.ArgumentNullException(nameof(document)); - if (writer is null) ThrowHelper.ArgumentNullException_writer(); + using var rentedRenderer = pipeline.RentHtmlRenderer(); + HtmlRenderer renderer = rentedRenderer.Instance; - pipeline ??= DefaultPipeline; + renderer.Render(document); + renderer.Writer.Flush(); - using var rentedRenderer = pipeline.RentHtmlRenderer(writer); - HtmlRenderer renderer = rentedRenderer.Instance; + return renderer.Writer.ToString() ?? string.Empty; + } - renderer.Render(document); - renderer.Writer.Flush(); - } + /// + /// Converts a Markdown document to HTML. + /// + /// A Markdown document. + /// The destination that will receive the result of the conversion. + /// The pipeline used for the conversion. + /// The result of the conversion + /// if markdown document variable is null + public static void ToHtml(this MarkdownDocument document, TextWriter writer, MarkdownPipeline? pipeline = null) + { + if (document is null) ThrowHelper.ArgumentNullException(nameof(document)); + if (writer is null) ThrowHelper.ArgumentNullException_writer(); - /// - /// Converts a Markdown string to HTML and output to the specified writer. - /// - /// A Markdown text. - /// The destination that will receive the result of the conversion. - /// The pipeline used for the conversion. - /// A parser context used for the parsing. - /// The Markdown document that has been parsed - /// if reader or writer variable are null - public static MarkdownDocument ToHtml(string markdown, TextWriter writer, MarkdownPipeline? pipeline = null, MarkdownParserContext? context = null) - { - if (markdown is null) ThrowHelper.ArgumentNullException_markdown(); - if (writer is null) ThrowHelper.ArgumentNullException_writer(); + pipeline ??= DefaultPipeline; - pipeline = GetPipeline(pipeline, markdown); + using var rentedRenderer = pipeline.RentHtmlRenderer(writer); + HtmlRenderer renderer = rentedRenderer.Instance; - var document = MarkdownParser.Parse(markdown, pipeline, context); + renderer.Render(document); + renderer.Writer.Flush(); + } - using var rentedRenderer = pipeline.RentHtmlRenderer(writer); - HtmlRenderer renderer = rentedRenderer.Instance; + /// + /// Converts a Markdown string to HTML and output to the specified writer. + /// + /// A Markdown text. + /// The destination that will receive the result of the conversion. + /// The pipeline used for the conversion. + /// A parser context used for the parsing. + /// The Markdown document that has been parsed + /// if reader or writer variable are null + public static MarkdownDocument ToHtml(string markdown, TextWriter writer, MarkdownPipeline? pipeline = null, MarkdownParserContext? context = null) + { + if (markdown is null) ThrowHelper.ArgumentNullException_markdown(); + if (writer is null) ThrowHelper.ArgumentNullException_writer(); - renderer.Render(document); - writer.Flush(); + pipeline = GetPipeline(pipeline, markdown); - return document; - } + var document = MarkdownParser.Parse(markdown, pipeline, context); - /// - /// Converts a Markdown string using a custom . - /// - /// A Markdown text. - /// The renderer to convert Markdown to. - /// The pipeline used for the conversion. - /// A parser context used for the parsing. - /// if markdown or writer variable are null - public static object Convert(string markdown, IMarkdownRenderer renderer, MarkdownPipeline? pipeline = null, MarkdownParserContext? context = null) - { - if (markdown is null) ThrowHelper.ArgumentNullException_markdown(); - if (renderer is null) ThrowHelper.ArgumentNullException(nameof(renderer)); + using var rentedRenderer = pipeline.RentHtmlRenderer(writer); + HtmlRenderer renderer = rentedRenderer.Instance; - pipeline = GetPipeline(pipeline, markdown); + renderer.Render(document); + writer.Flush(); - var document = MarkdownParser.Parse(markdown, pipeline, context); + return document; + } - pipeline.Setup(renderer); - return renderer.Render(document); - } + /// + /// Converts a Markdown string using a custom . + /// + /// A Markdown text. + /// The renderer to convert Markdown to. + /// The pipeline used for the conversion. + /// A parser context used for the parsing. + /// if markdown or writer variable are null + public static object Convert(string markdown, IMarkdownRenderer renderer, MarkdownPipeline? pipeline = null, MarkdownParserContext? context = null) + { + if (markdown is null) ThrowHelper.ArgumentNullException_markdown(); + if (renderer is null) ThrowHelper.ArgumentNullException(nameof(renderer)); - /// - /// Parses the specified markdown into an AST - /// - /// The markdown text. - /// Whether to parse trivia such as whitespace, extra heading characters and unescaped string values. - /// An AST Markdown document - /// if markdown variable is null - public static MarkdownDocument Parse(string markdown, bool trackTrivia = false) - { - if (markdown is null) ThrowHelper.ArgumentNullException_markdown(); + pipeline = GetPipeline(pipeline, markdown); - MarkdownPipeline? pipeline = trackTrivia ? _defaultTrackTriviaPipeline : null; + var document = MarkdownParser.Parse(markdown, pipeline, context); - return Parse(markdown, pipeline); - } + pipeline.Setup(renderer); + return renderer.Render(document); + } - /// - /// Parses the specified markdown into an AST - /// - /// The markdown text. - /// The pipeline used for the parsing. - /// A parser context used for the parsing. - /// An AST Markdown document - /// if markdown variable is null - public static MarkdownDocument Parse(string markdown, MarkdownPipeline? pipeline, MarkdownParserContext? context = null) - { - if (markdown is null) ThrowHelper.ArgumentNullException_markdown(); + /// + /// Parses the specified markdown into an AST + /// + /// The markdown text. + /// Whether to parse trivia such as whitespace, extra heading characters and unescaped string values. + /// An AST Markdown document + /// if markdown variable is null + public static MarkdownDocument Parse(string markdown, bool trackTrivia = false) + { + if (markdown is null) ThrowHelper.ArgumentNullException_markdown(); - pipeline = GetPipeline(pipeline, markdown); + MarkdownPipeline? pipeline = trackTrivia ? _defaultTrackTriviaPipeline : null; - return MarkdownParser.Parse(markdown, pipeline, context); - } + return Parse(markdown, pipeline); + } - /// - /// Converts a Markdown string to Plain text and output to the specified writer. - /// - /// A Markdown text. - /// The destination that will receive the result of the conversion. - /// The pipeline used for the conversion. - /// A parser context used for the parsing. - /// The Markdown document that has been parsed - /// if reader or writer variable are null - public static MarkdownDocument ToPlainText(string markdown, TextWriter writer, MarkdownPipeline? pipeline = null, MarkdownParserContext? context = null) - { - if (markdown is null) ThrowHelper.ArgumentNullException_markdown(); - if (writer is null) ThrowHelper.ArgumentNullException_writer(); + /// + /// Parses the specified markdown into an AST + /// + /// The markdown text. + /// The pipeline used for the parsing. + /// A parser context used for the parsing. + /// An AST Markdown document + /// if markdown variable is null + public static MarkdownDocument Parse(string markdown, MarkdownPipeline? pipeline, MarkdownParserContext? context = null) + { + if (markdown is null) ThrowHelper.ArgumentNullException_markdown(); - pipeline = GetPipeline(pipeline, markdown); + pipeline = GetPipeline(pipeline, markdown); - var document = MarkdownParser.Parse(markdown, pipeline, context); + return MarkdownParser.Parse(markdown, pipeline, context); + } - // We override the renderer with our own writer - var renderer = new HtmlRenderer(writer) - { - EnableHtmlForBlock = false, - EnableHtmlForInline = false, - EnableHtmlEscape = false, - }; - pipeline.Setup(renderer); + /// + /// Converts a Markdown string to Plain text and output to the specified writer. + /// + /// A Markdown text. + /// The destination that will receive the result of the conversion. + /// The pipeline used for the conversion. + /// A parser context used for the parsing. + /// The Markdown document that has been parsed + /// if reader or writer variable are null + public static MarkdownDocument ToPlainText(string markdown, TextWriter writer, MarkdownPipeline? pipeline = null, MarkdownParserContext? context = null) + { + if (markdown is null) ThrowHelper.ArgumentNullException_markdown(); + if (writer is null) ThrowHelper.ArgumentNullException_writer(); - renderer.Render(document); - writer.Flush(); + pipeline = GetPipeline(pipeline, markdown); - return document; - } + var document = MarkdownParser.Parse(markdown, pipeline, context); - /// - /// Converts a Markdown string to HTML. - /// - /// A Markdown text. - /// The pipeline used for the conversion. - /// A parser context used for the parsing. - /// The result of the conversion - /// if markdown variable is null - public static string ToPlainText(string markdown, MarkdownPipeline? pipeline = null, MarkdownParserContext? context = null) + // We override the renderer with our own writer + var renderer = new HtmlRenderer(writer) { - if (markdown is null) ThrowHelper.ArgumentNullException_markdown(); - var writer = new StringWriter(); - ToPlainText(markdown, writer, pipeline, context); - return writer.ToString(); - } + EnableHtmlForBlock = false, + EnableHtmlForInline = false, + EnableHtmlEscape = false, + }; + pipeline.Setup(renderer); + + renderer.Render(document); + writer.Flush(); + + return document; + } + + /// + /// Converts a Markdown string to HTML. + /// + /// A Markdown text. + /// The pipeline used for the conversion. + /// A parser context used for the parsing. + /// The result of the conversion + /// if markdown variable is null + public static string ToPlainText(string markdown, MarkdownPipeline? pipeline = null, MarkdownParserContext? context = null) + { + if (markdown is null) ThrowHelper.ArgumentNullException_markdown(); + var writer = new StringWriter(); + ToPlainText(markdown, writer, pipeline, context); + return writer.ToString(); } } diff --git a/src/Markdig/MarkdownExtensions.cs b/src/Markdig/MarkdownExtensions.cs index 75a60463a..261bbe313 100644 --- a/src/Markdig/MarkdownExtensions.cs +++ b/src/Markdig/MarkdownExtensions.cs @@ -2,7 +2,6 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using System; using Markdig.Extensions.Abbreviations; using Markdig.Extensions.AutoIdentifiers; using Markdig.Extensions.AutoLinks; @@ -17,679 +16,678 @@ using Markdig.Extensions.Footers; using Markdig.Extensions.Footnotes; using Markdig.Extensions.GenericAttributes; +using Markdig.Extensions.Globalization; using Markdig.Extensions.Hardlines; using Markdig.Extensions.JiraLinks; using Markdig.Extensions.ListExtras; using Markdig.Extensions.Mathematics; using Markdig.Extensions.MediaLinks; +using Markdig.Extensions.NonAsciiNoEscape; using Markdig.Extensions.PragmaLines; +using Markdig.Extensions.ReferralLinks; using Markdig.Extensions.SelfPipeline; using Markdig.Extensions.SmartyPants; -using Markdig.Extensions.NonAsciiNoEscape; using Markdig.Extensions.Tables; using Markdig.Extensions.TaskLists; using Markdig.Extensions.TextRenderer; using Markdig.Extensions.Yaml; +using Markdig.Helpers; using Markdig.Parsers; using Markdig.Parsers.Inlines; -using Markdig.Extensions.Globalization; -using Markdig.Helpers; -using Markdig.Extensions.ReferralLinks; -namespace Markdig +namespace Markdig; + +/// +/// Provides extension methods for to enable several Markdown extensions. +/// +public static class MarkdownExtensions { /// - /// Provides extension methods for to enable several Markdown extensions. + /// Adds the specified extension to the extensions collection. /// - public static class MarkdownExtensions + /// The type of the extension. + /// The instance of + public static MarkdownPipelineBuilder Use(this MarkdownPipelineBuilder pipeline) where TExtension : class, IMarkdownExtension, new() { - /// - /// Adds the specified extension to the extensions collection. - /// - /// The type of the extension. - /// The instance of - public static MarkdownPipelineBuilder Use(this MarkdownPipelineBuilder pipeline) where TExtension : class, IMarkdownExtension, new() - { - pipeline.Extensions.AddIfNotAlready(); - return pipeline; - } + pipeline.Extensions.AddIfNotAlready(); + return pipeline; + } - /// - /// Adds the specified extension instance to the extensions collection. - /// - /// The pipeline. - /// The instance of the extension to be added. - /// The type of the extension. - /// The modified pipeline - public static MarkdownPipelineBuilder Use(this MarkdownPipelineBuilder pipeline, TExtension extension) where TExtension : class, IMarkdownExtension - { - pipeline.Extensions.AddIfNotAlready(extension); - return pipeline; - } + /// + /// Adds the specified extension instance to the extensions collection. + /// + /// The pipeline. + /// The instance of the extension to be added. + /// The type of the extension. + /// The modified pipeline + public static MarkdownPipelineBuilder Use(this MarkdownPipelineBuilder pipeline, TExtension extension) where TExtension : class, IMarkdownExtension + { + pipeline.Extensions.AddIfNotAlready(extension); + return pipeline; + } - /// - /// Uses all extensions except the BootStrap, Emoji, SmartyPants and soft line as hard line breaks extensions. - /// - /// The pipeline. - /// The modified pipeline - public static MarkdownPipelineBuilder UseAdvancedExtensions(this MarkdownPipelineBuilder pipeline) - { - return pipeline - .UseAbbreviations() - .UseAutoIdentifiers() - .UseCitations() - .UseCustomContainers() - .UseDefinitionLists() - .UseEmphasisExtras() - .UseFigures() - .UseFooters() - .UseFootnotes() - .UseGridTables() - .UseMathematics() - .UseMediaLinks() - .UsePipeTables() - .UseListExtras() - .UseTaskLists() - .UseDiagrams() - .UseAutoLinks() - .UseGenericAttributes(); // Must be last as it is one parser that is modifying other parsers - } + /// + /// Uses all extensions except the BootStrap, Emoji, SmartyPants and soft line as hard line breaks extensions. + /// + /// The pipeline. + /// The modified pipeline + public static MarkdownPipelineBuilder UseAdvancedExtensions(this MarkdownPipelineBuilder pipeline) + { + return pipeline + .UseAbbreviations() + .UseAutoIdentifiers() + .UseCitations() + .UseCustomContainers() + .UseDefinitionLists() + .UseEmphasisExtras() + .UseFigures() + .UseFooters() + .UseFootnotes() + .UseGridTables() + .UseMathematics() + .UseMediaLinks() + .UsePipeTables() + .UseListExtras() + .UseTaskLists() + .UseDiagrams() + .UseAutoLinks() + .UseGenericAttributes(); // Must be last as it is one parser that is modifying other parsers + } - /// - /// Uses this extension to enable autolinks from text `http://`, `https://`, `ftp://`, `mailto:`, `www.xxx.yyy` - /// - /// The pipeline. - /// The options. - /// The modified pipeline - public static MarkdownPipelineBuilder UseAutoLinks(this MarkdownPipelineBuilder pipeline, AutoLinkOptions? options = null) - { - pipeline.Extensions.ReplaceOrAdd(new AutoLinkExtension(options)); - return pipeline; - } + /// + /// Uses this extension to enable autolinks from text `http://`, `https://`, `ftp://`, `mailto:`, `www.xxx.yyy` + /// + /// The pipeline. + /// The options. + /// The modified pipeline + public static MarkdownPipelineBuilder UseAutoLinks(this MarkdownPipelineBuilder pipeline, AutoLinkOptions? options = null) + { + pipeline.Extensions.ReplaceOrAdd(new AutoLinkExtension(options)); + return pipeline; + } - /// - /// Uses this extension to disable URI escape with % characters for non-US-ASCII characters in order to workaround a bug under IE/Edge with local file links containing non US-ASCII chars. DO NOT USE OTHERWISE. - /// - /// The pipeline. - /// The modified pipeline - public static MarkdownPipelineBuilder UseNonAsciiNoEscape(this MarkdownPipelineBuilder pipeline) - { - pipeline.Extensions.AddIfNotAlready(); - return pipeline; - } + /// + /// Uses this extension to disable URI escape with % characters for non-US-ASCII characters in order to workaround a bug under IE/Edge with local file links containing non US-ASCII chars. DO NOT USE OTHERWISE. + /// + /// The pipeline. + /// The modified pipeline + public static MarkdownPipelineBuilder UseNonAsciiNoEscape(this MarkdownPipelineBuilder pipeline) + { + pipeline.Extensions.AddIfNotAlready(); + return pipeline; + } + + /// + /// Uses YAML frontmatter extension that will parse a YAML frontmatter into the MarkdownDocument. Note that they are not rendered by any default HTML renderer. + /// + /// The pipeline. + /// The modified pipeline + public static MarkdownPipelineBuilder UseYamlFrontMatter(this MarkdownPipelineBuilder pipeline) + { + pipeline.Extensions.AddIfNotAlready(); + return pipeline; + } - /// - /// Uses YAML frontmatter extension that will parse a YAML frontmatter into the MarkdownDocument. Note that they are not rendered by any default HTML renderer. - /// - /// The pipeline. - /// The modified pipeline - public static MarkdownPipelineBuilder UseYamlFrontMatter(this MarkdownPipelineBuilder pipeline) + /// + /// Uses the self pipeline extension that will detect the pipeline to use from the markdown input that contains a special tag. See + /// + /// The pipeline. + /// The default tag to use to match the self pipeline configuration. By default, , meaning that the HTML tag will be <--markdig:extensions--> + /// The default extensions to configure if no pipeline setup was found from the Markdown document + /// The modified pipeline + public static MarkdownPipelineBuilder UseSelfPipeline(this MarkdownPipelineBuilder pipeline, string defaultTag = SelfPipelineExtension.DefaultTag, string? defaultExtensions = null) + { + if (pipeline.Extensions.Count != 0) { - pipeline.Extensions.AddIfNotAlready(); - return pipeline; + ThrowHelper.InvalidOperationException("The SelfPipeline extension cannot be used with other extensions"); } - /// - /// Uses the self pipeline extension that will detect the pipeline to use from the markdown input that contains a special tag. See - /// - /// The pipeline. - /// The default tag to use to match the self pipeline configuration. By default, , meaning that the HTML tag will be <--markdig:extensions--> - /// The default extensions to configure if no pipeline setup was found from the Markdown document - /// The modified pipeline - public static MarkdownPipelineBuilder UseSelfPipeline(this MarkdownPipelineBuilder pipeline, string defaultTag = SelfPipelineExtension.DefaultTag, string? defaultExtensions = null) - { - if (pipeline.Extensions.Count != 0) - { - ThrowHelper.InvalidOperationException("The SelfPipeline extension cannot be used with other extensions"); - } + pipeline.Extensions.Add(new SelfPipelineExtension(defaultTag, defaultExtensions)); + return pipeline; + } - pipeline.Extensions.Add(new SelfPipelineExtension(defaultTag, defaultExtensions)); - return pipeline; - } + /// + /// Uses pragma lines to output span with an id containing the line number (pragma-line#line_number_zero_based`) + /// + /// The pipeline. + /// The modified pipeline + public static MarkdownPipelineBuilder UsePragmaLines(this MarkdownPipelineBuilder pipeline) + { + pipeline.Extensions.AddIfNotAlready(); + return pipeline; + } - /// - /// Uses pragma lines to output span with an id containing the line number (pragma-line#line_number_zero_based`) - /// - /// The pipeline. - /// The modified pipeline - public static MarkdownPipelineBuilder UsePragmaLines(this MarkdownPipelineBuilder pipeline) - { - pipeline.Extensions.AddIfNotAlready(); - return pipeline; - } + /// + /// Uses the diagrams extension + /// + /// The pipeline. + /// The modified pipeline + public static MarkdownPipelineBuilder UseDiagrams(this MarkdownPipelineBuilder pipeline) + { + pipeline.Extensions.AddIfNotAlready(); + return pipeline; + } - /// - /// Uses the diagrams extension - /// - /// The pipeline. - /// The modified pipeline - public static MarkdownPipelineBuilder UseDiagrams(this MarkdownPipelineBuilder pipeline) - { - pipeline.Extensions.AddIfNotAlready(); - return pipeline; - } + /// + /// Uses precise source code location (useful for syntax highlighting). + /// + /// The pipeline. + /// The modified pipeline + public static MarkdownPipelineBuilder UsePreciseSourceLocation(this MarkdownPipelineBuilder pipeline) + { + pipeline.PreciseSourceLocation = true; + return pipeline; + } - /// - /// Uses precise source code location (useful for syntax highlighting). - /// - /// The pipeline. - /// The modified pipeline - public static MarkdownPipelineBuilder UsePreciseSourceLocation(this MarkdownPipelineBuilder pipeline) - { - pipeline.PreciseSourceLocation = true; - return pipeline; - } + /// + /// Uses the task list extension. + /// + /// The pipeline. + /// The modified pipeline + public static MarkdownPipelineBuilder UseTaskLists(this MarkdownPipelineBuilder pipeline) + { + pipeline.Extensions.AddIfNotAlready(); + return pipeline; + } - /// - /// Uses the task list extension. - /// - /// The pipeline. - /// The modified pipeline - public static MarkdownPipelineBuilder UseTaskLists(this MarkdownPipelineBuilder pipeline) - { - pipeline.Extensions.AddIfNotAlready(); - return pipeline; - } + /// + /// Uses the custom container extension. + /// + /// The pipeline. + /// The modified pipeline + public static MarkdownPipelineBuilder UseCustomContainers(this MarkdownPipelineBuilder pipeline) + { + pipeline.Extensions.AddIfNotAlready(); + return pipeline; + } - /// - /// Uses the custom container extension. - /// - /// The pipeline. - /// The modified pipeline - public static MarkdownPipelineBuilder UseCustomContainers(this MarkdownPipelineBuilder pipeline) + /// + /// Uses the media extension. + /// + /// The pipeline. + /// The options. + /// + /// The modified pipeline + /// + public static MarkdownPipelineBuilder UseMediaLinks(this MarkdownPipelineBuilder pipeline, MediaOptions? options = null) + { + if (!pipeline.Extensions.Contains()) { - pipeline.Extensions.AddIfNotAlready(); - return pipeline; + pipeline.Extensions.Add(new MediaLinkExtension(options)); } + return pipeline; + } - /// - /// Uses the media extension. - /// - /// The pipeline. - /// The options. - /// - /// The modified pipeline - /// - public static MarkdownPipelineBuilder UseMediaLinks(this MarkdownPipelineBuilder pipeline, MediaOptions? options = null) + /// + /// Uses the auto-identifier extension. + /// + /// The pipeline. + /// The options. + /// + /// The modified pipeline + /// + public static MarkdownPipelineBuilder UseAutoIdentifiers(this MarkdownPipelineBuilder pipeline, AutoIdentifierOptions options = AutoIdentifierOptions.Default) + { + if (!pipeline.Extensions.Contains()) { - if (!pipeline.Extensions.Contains()) - { - pipeline.Extensions.Add(new MediaLinkExtension(options)); - } - return pipeline; + pipeline.Extensions.Add(new AutoIdentifierExtension(options)); } + return pipeline; + } - /// - /// Uses the auto-identifier extension. - /// - /// The pipeline. - /// The options. - /// - /// The modified pipeline - /// - public static MarkdownPipelineBuilder UseAutoIdentifiers(this MarkdownPipelineBuilder pipeline, AutoIdentifierOptions options = AutoIdentifierOptions.Default) + /// + /// Uses the SmartyPants extension. + /// + /// The pipeline. + /// The options. + /// + /// The modified pipeline + /// + public static MarkdownPipelineBuilder UseSmartyPants(this MarkdownPipelineBuilder pipeline, SmartyPantOptions? options = null) + { + if (!pipeline.Extensions.Contains()) { - if (!pipeline.Extensions.Contains()) - { - pipeline.Extensions.Add(new AutoIdentifierExtension(options)); - } - return pipeline; + pipeline.Extensions.Add(new SmartyPantsExtension(options)); } + return pipeline; + } - /// - /// Uses the SmartyPants extension. - /// - /// The pipeline. - /// The options. - /// - /// The modified pipeline - /// - public static MarkdownPipelineBuilder UseSmartyPants(this MarkdownPipelineBuilder pipeline, SmartyPantOptions? options = null) - { - if (!pipeline.Extensions.Contains()) - { - pipeline.Extensions.Add(new SmartyPantsExtension(options)); - } - return pipeline; - } + /// + /// Uses the bootstrap extension. + /// + /// The pipeline. + /// The modified pipeline + public static MarkdownPipelineBuilder UseBootstrap(this MarkdownPipelineBuilder pipeline) + { + pipeline.Extensions.AddIfNotAlready(); + return pipeline; + } - /// - /// Uses the bootstrap extension. - /// - /// The pipeline. - /// The modified pipeline - public static MarkdownPipelineBuilder UseBootstrap(this MarkdownPipelineBuilder pipeline) - { - pipeline.Extensions.AddIfNotAlready(); - return pipeline; - } + /// + /// Uses the math extension. + /// + /// The pipeline. + /// The modified pipeline + public static MarkdownPipelineBuilder UseMathematics(this MarkdownPipelineBuilder pipeline) + { + pipeline.Extensions.AddIfNotAlready(); + return pipeline; + } - /// - /// Uses the math extension. - /// - /// The pipeline. - /// The modified pipeline - public static MarkdownPipelineBuilder UseMathematics(this MarkdownPipelineBuilder pipeline) - { - pipeline.Extensions.AddIfNotAlready(); - return pipeline; - } + /// + /// Uses the figure extension. + /// + /// The pipeline. + /// The modified pipeline + public static MarkdownPipelineBuilder UseFigures(this MarkdownPipelineBuilder pipeline) + { + pipeline.Extensions.AddIfNotAlready(); + return pipeline; + } - /// - /// Uses the figure extension. - /// - /// The pipeline. - /// The modified pipeline - public static MarkdownPipelineBuilder UseFigures(this MarkdownPipelineBuilder pipeline) - { - pipeline.Extensions.AddIfNotAlready(); - return pipeline; - } + /// + /// Uses the custom abbreviation extension. + /// + /// The pipeline. + /// The modified pipeline + public static MarkdownPipelineBuilder UseAbbreviations(this MarkdownPipelineBuilder pipeline) + { + pipeline.Extensions.AddIfNotAlready(); + return pipeline; + } - /// - /// Uses the custom abbreviation extension. - /// - /// The pipeline. - /// The modified pipeline - public static MarkdownPipelineBuilder UseAbbreviations(this MarkdownPipelineBuilder pipeline) - { - pipeline.Extensions.AddIfNotAlready(); - return pipeline; - } + /// + /// Uses the definition lists extension. + /// + /// The pipeline. + /// The modified pipeline + public static MarkdownPipelineBuilder UseDefinitionLists(this MarkdownPipelineBuilder pipeline) + { + pipeline.Extensions.AddIfNotAlready(); + return pipeline; + } - /// - /// Uses the definition lists extension. - /// - /// The pipeline. - /// The modified pipeline - public static MarkdownPipelineBuilder UseDefinitionLists(this MarkdownPipelineBuilder pipeline) + /// + /// Uses the pipe table extension. + /// + /// The pipeline. + /// The options. + /// + /// The modified pipeline + /// + public static MarkdownPipelineBuilder UsePipeTables(this MarkdownPipelineBuilder pipeline, PipeTableOptions? options = null) + { + if (!pipeline.Extensions.Contains()) { - pipeline.Extensions.AddIfNotAlready(); - return pipeline; + pipeline.Extensions.Add(new PipeTableExtension(options)); } + return pipeline; + } - /// - /// Uses the pipe table extension. - /// - /// The pipeline. - /// The options. - /// - /// The modified pipeline - /// - public static MarkdownPipelineBuilder UsePipeTables(this MarkdownPipelineBuilder pipeline, PipeTableOptions? options = null) - { - if (!pipeline.Extensions.Contains()) - { - pipeline.Extensions.Add(new PipeTableExtension(options)); - } - return pipeline; - } + /// + /// Uses the grid table extension. + /// + /// The pipeline. + /// The modified pipeline + public static MarkdownPipelineBuilder UseGridTables(this MarkdownPipelineBuilder pipeline) + { + pipeline.Extensions.AddIfNotAlready(); + return pipeline; + } - /// - /// Uses the grid table extension. - /// - /// The pipeline. - /// The modified pipeline - public static MarkdownPipelineBuilder UseGridTables(this MarkdownPipelineBuilder pipeline) - { - pipeline.Extensions.AddIfNotAlready(); - return pipeline; - } + /// + /// Uses the cite extension. + /// + /// The pipeline. + /// The modified pipeline + public static MarkdownPipelineBuilder UseCitations(this MarkdownPipelineBuilder pipeline) + { + pipeline.Extensions.AddIfNotAlready(); + return pipeline; + } - /// - /// Uses the cite extension. - /// - /// The pipeline. - /// The modified pipeline - public static MarkdownPipelineBuilder UseCitations(this MarkdownPipelineBuilder pipeline) - { - pipeline.Extensions.AddIfNotAlready(); - return pipeline; - } + /// + /// Uses the footer extension. + /// + /// The pipeline. + /// The modified pipeline + public static MarkdownPipelineBuilder UseFooters(this MarkdownPipelineBuilder pipeline) + { + pipeline.Extensions.AddIfNotAlready(); + return pipeline; + } - /// - /// Uses the footer extension. - /// - /// The pipeline. - /// The modified pipeline - public static MarkdownPipelineBuilder UseFooters(this MarkdownPipelineBuilder pipeline) - { - pipeline.Extensions.AddIfNotAlready(); - return pipeline; - } + /// + /// Uses the footnotes extension. + /// + /// The pipeline. + /// The modified pipeline + public static MarkdownPipelineBuilder UseFootnotes(this MarkdownPipelineBuilder pipeline) + { + pipeline.Extensions.AddIfNotAlready(); + return pipeline; + } - /// - /// Uses the footnotes extension. - /// - /// The pipeline. - /// The modified pipeline - public static MarkdownPipelineBuilder UseFootnotes(this MarkdownPipelineBuilder pipeline) - { - pipeline.Extensions.AddIfNotAlready(); - return pipeline; - } + /// + /// Uses the softline break as hardline break extension + /// + /// The pipeline. + /// The modified pipeline + public static MarkdownPipelineBuilder UseSoftlineBreakAsHardlineBreak(this MarkdownPipelineBuilder pipeline) + { + pipeline.Extensions.AddIfNotAlready(); + return pipeline; + } - /// - /// Uses the softline break as hardline break extension - /// - /// The pipeline. - /// The modified pipeline - public static MarkdownPipelineBuilder UseSoftlineBreakAsHardlineBreak(this MarkdownPipelineBuilder pipeline) + /// + /// Uses the strikethrough superscript, subscript, inserted and marked text extensions. + /// + /// The pipeline. + /// The options to enable. + /// + /// The modified pipeline + /// + public static MarkdownPipelineBuilder UseEmphasisExtras(this MarkdownPipelineBuilder pipeline, EmphasisExtraOptions options = EmphasisExtraOptions.Default) + { + if (!pipeline.Extensions.Contains()) { - pipeline.Extensions.AddIfNotAlready(); - return pipeline; + pipeline.Extensions.Add(new EmphasisExtraExtension(options)); } + return pipeline; + } - /// - /// Uses the strikethrough superscript, subscript, inserted and marked text extensions. - /// - /// The pipeline. - /// The options to enable. - /// - /// The modified pipeline - /// - public static MarkdownPipelineBuilder UseEmphasisExtras(this MarkdownPipelineBuilder pipeline, EmphasisExtraOptions options = EmphasisExtraOptions.Default) - { - if (!pipeline.Extensions.Contains()) - { - pipeline.Extensions.Add(new EmphasisExtraExtension(options)); - } - return pipeline; - } + /// + /// Uses the list extra extension to add support for `a.`, `A.`, `i.` and `I.` ordered list items. + /// + /// The pipeline. + /// + /// The modified pipeline + /// + public static MarkdownPipelineBuilder UseListExtras(this MarkdownPipelineBuilder pipeline) + { + pipeline.Extensions.AddIfNotAlready(); + return pipeline; + } - /// - /// Uses the list extra extension to add support for `a.`, `A.`, `i.` and `I.` ordered list items. - /// - /// The pipeline. - /// - /// The modified pipeline - /// - public static MarkdownPipelineBuilder UseListExtras(this MarkdownPipelineBuilder pipeline) - { - pipeline.Extensions.AddIfNotAlready(); - return pipeline; - } + /// + /// Uses the generic attributes extension. + /// + /// The pipeline. + /// The modified pipeline + public static MarkdownPipelineBuilder UseGenericAttributes(this MarkdownPipelineBuilder pipeline) + { + pipeline.Extensions.AddIfNotAlready(); + return pipeline; + } - /// - /// Uses the generic attributes extension. - /// - /// The pipeline. - /// The modified pipeline - public static MarkdownPipelineBuilder UseGenericAttributes(this MarkdownPipelineBuilder pipeline) + /// + /// Uses the emojis and smileys extension. + /// + /// The pipeline. + /// Enable smileys in addition to emoji shortcodes, true by default. + /// The modified pipeline + public static MarkdownPipelineBuilder UseEmojiAndSmiley(this MarkdownPipelineBuilder pipeline, bool enableSmileys = true) + { + if (!pipeline.Extensions.Contains()) { - pipeline.Extensions.AddIfNotAlready(); - return pipeline; + var emojiMapping = enableSmileys ? EmojiMapping.DefaultEmojisAndSmileysMapping : EmojiMapping.DefaultEmojisOnlyMapping; + pipeline.Extensions.Add(new EmojiExtension(emojiMapping)); } + return pipeline; + } - /// - /// Uses the emojis and smileys extension. - /// - /// The pipeline. - /// Enable smileys in addition to emoji shortcodes, true by default. - /// The modified pipeline - public static MarkdownPipelineBuilder UseEmojiAndSmiley(this MarkdownPipelineBuilder pipeline, bool enableSmileys = true) + /// + /// Uses the emojis and smileys extension. + /// + /// The pipeline. + /// Enable customization of the emojis and smileys mapping. + /// The modified pipeline + public static MarkdownPipelineBuilder UseEmojiAndSmiley(this MarkdownPipelineBuilder pipeline, EmojiMapping customEmojiMapping) + { + if (!pipeline.Extensions.Contains()) { - if (!pipeline.Extensions.Contains()) - { - var emojiMapping = enableSmileys ? EmojiMapping.DefaultEmojisAndSmileysMapping : EmojiMapping.DefaultEmojisOnlyMapping; - pipeline.Extensions.Add(new EmojiExtension(emojiMapping)); - } - return pipeline; + pipeline.Extensions.Add(new EmojiExtension(customEmojiMapping)); } + return pipeline; + } + /// + /// Add rel=nofollow to all links rendered to HTML. + /// + /// + /// + [Obsolete("Call `UseReferralLinks(\"nofollow\")` instead")] + public static MarkdownPipelineBuilder UseNoFollowLinks(this MarkdownPipelineBuilder pipeline) + { + return pipeline.UseReferralLinks("nofollow"); + } - /// - /// Uses the emojis and smileys extension. - /// - /// The pipeline. - /// Enable customization of the emojis and smileys mapping. - /// The modified pipeline - public static MarkdownPipelineBuilder UseEmojiAndSmiley(this MarkdownPipelineBuilder pipeline, EmojiMapping customEmojiMapping) + public static MarkdownPipelineBuilder UseReferralLinks(this MarkdownPipelineBuilder pipeline, params string[] rels) + { + if (pipeline.Extensions.TryFind(out ReferralLinksExtension? referralLinksExtension)) { - if (!pipeline.Extensions.Contains()) + foreach (string rel in rels) { - pipeline.Extensions.Add(new EmojiExtension(customEmojiMapping)); + if (!referralLinksExtension.Rels.Contains(rel)) + { + referralLinksExtension.Rels.Add(rel); + } } - return pipeline; } - /// - /// Add rel=nofollow to all links rendered to HTML. - /// - /// - /// - [Obsolete("Call `UseReferralLinks(\"nofollow\")` instead")] - public static MarkdownPipelineBuilder UseNoFollowLinks(this MarkdownPipelineBuilder pipeline) + else { - return pipeline.UseReferralLinks("nofollow"); + pipeline.Extensions.Add(new ReferralLinksExtension(rels)); } + return pipeline; + } - public static MarkdownPipelineBuilder UseReferralLinks(this MarkdownPipelineBuilder pipeline, params string[] rels) + /// + /// Automatically link references to JIRA issues + /// + /// The pipeline + /// Set of required options + /// The modified pipeline + public static MarkdownPipelineBuilder UseJiraLinks(this MarkdownPipelineBuilder pipeline, JiraLinkOptions options) + { + if (!pipeline.Extensions.Contains()) { - if (pipeline.Extensions.TryFind(out ReferralLinksExtension? referralLinksExtension)) - { - foreach (string rel in rels) - { - if (!referralLinksExtension.Rels.Contains(rel)) - { - referralLinksExtension.Rels.Add(rel); - } - } - } - else - { - pipeline.Extensions.Add(new ReferralLinksExtension(rels)); - } - return pipeline; + pipeline.Extensions.Add(new JiraLinkExtension(options)); } + return pipeline; + } + + /// + /// Adds support for right-to-left content by adding appropriate html attribtues. + /// + /// The pipeline + /// The modified pipeline + public static MarkdownPipelineBuilder UseGlobalization(this MarkdownPipelineBuilder pipeline) + { + pipeline.Extensions.AddIfNotAlready(); + return pipeline; + } - /// - /// Automatically link references to JIRA issues - /// - /// The pipeline - /// Set of required options - /// The modified pipeline - public static MarkdownPipelineBuilder UseJiraLinks(this MarkdownPipelineBuilder pipeline, JiraLinkOptions options) + /// + /// This will disable the HTML support in the markdown processor (for constraint/safe parsing). + /// + /// The pipeline. + /// The modified pipeline + public static MarkdownPipelineBuilder DisableHtml(this MarkdownPipelineBuilder pipeline) + { + var parser = pipeline.BlockParsers.Find(); + if (parser != null) { - if (!pipeline.Extensions.Contains()) - { - pipeline.Extensions.Add(new JiraLinkExtension(options)); - } - return pipeline; + pipeline.BlockParsers.Remove(parser); } - /// - /// Adds support for right-to-left content by adding appropriate html attribtues. - /// - /// The pipeline - /// The modified pipeline - public static MarkdownPipelineBuilder UseGlobalization(this MarkdownPipelineBuilder pipeline) + var inlineParser = pipeline.InlineParsers.Find(); + if (inlineParser != null) { - pipeline.Extensions.AddIfNotAlready(); - return pipeline; + inlineParser.EnableHtmlParsing = false; } + return pipeline; + } - /// - /// This will disable the HTML support in the markdown processor (for constraint/safe parsing). - /// - /// The pipeline. - /// The modified pipeline - public static MarkdownPipelineBuilder DisableHtml(this MarkdownPipelineBuilder pipeline) + /// + /// Configures the pipeline using a string that defines the extensions to activate. + /// + /// The pipeline (e.g: advanced for , pipetables+gridtables for and + /// The extensions to activate as a string + /// The modified pipeline + public static MarkdownPipelineBuilder Configure(this MarkdownPipelineBuilder pipeline, string? extensions) + { + if (extensions is null) { - var parser = pipeline.BlockParsers.Find(); - if (parser != null) - { - pipeline.BlockParsers.Remove(parser); - } - - var inlineParser = pipeline.InlineParsers.Find(); - if (inlineParser != null) - { - inlineParser.EnableHtmlParsing = false; - } return pipeline; } - /// - /// Configures the pipeline using a string that defines the extensions to activate. - /// - /// The pipeline (e.g: advanced for , pipetables+gridtables for and - /// The extensions to activate as a string - /// The modified pipeline - public static MarkdownPipelineBuilder Configure(this MarkdownPipelineBuilder pipeline, string? extensions) - { - if (extensions is null) - { - return pipeline; - } - - // TODO: the extension string should come from the extension itself instead of this hardcoded switch case. + // TODO: the extension string should come from the extension itself instead of this hardcoded switch case. - foreach (var extension in extensions.Split(new[] { '+' }, StringSplitOptions.RemoveEmptyEntries)) + foreach (var extension in extensions.Split(new[] { '+' }, StringSplitOptions.RemoveEmptyEntries)) + { + switch (extension.ToLowerInvariant()) { - switch (extension.ToLowerInvariant()) - { - case "common": - break; - case "advanced": - pipeline.UseAdvancedExtensions(); - break; - case "pipetables": - pipeline.UsePipeTables(); - break; - case "gfm-pipetables": - pipeline.UsePipeTables(new PipeTableOptions { UseHeaderForColumnCount = true }); - break; - case "emphasisextras": - pipeline.UseEmphasisExtras(); - break; - case "listextras": - pipeline.UseListExtras(); - break; - case "hardlinebreak": - pipeline.UseSoftlineBreakAsHardlineBreak(); - break; - case "footnotes": - pipeline.UseFootnotes(); - break; - case "footers": - pipeline.UseFooters(); - break; - case "citations": - pipeline.UseCitations(); - break; - case "attributes": - pipeline.UseGenericAttributes(); - break; - case "gridtables": - pipeline.UseGridTables(); - break; - case "abbreviations": - pipeline.UseAbbreviations(); - break; - case "emojis": - pipeline.UseEmojiAndSmiley(); - break; - case "definitionlists": - pipeline.UseDefinitionLists(); - break; - case "customcontainers": - pipeline.UseCustomContainers(); - break; - case "figures": - pipeline.UseFigures(); - break; - case "mathematics": - pipeline.UseMathematics(); - break; - case "bootstrap": - pipeline.UseBootstrap(); - break; - case "medialinks": - pipeline.UseMediaLinks(); - break; - case "smartypants": - pipeline.UseSmartyPants(); - break; - case "autoidentifiers": - pipeline.UseAutoIdentifiers(); - break; - case "tasklists": - pipeline.UseTaskLists(); - break; - case "diagrams": - pipeline.UseDiagrams(); - break; - case "nofollowlinks": - pipeline.UseReferralLinks("nofollow"); - break; - case "noopenerlinks": - pipeline.UseReferralLinks("noopener"); - break; - case "noreferrerlinks": - pipeline.UseReferralLinks("noreferrer"); - break; - case "nohtml": - pipeline.DisableHtml(); - break; - case "yaml": - pipeline.UseYamlFrontMatter(); - break; - case "nonascii-noescape": - pipeline.UseNonAsciiNoEscape(); - break; - case "autolinks": - pipeline.UseAutoLinks(); - break; - case "globalization": - pipeline.UseGlobalization(); - break; - default: - throw new ArgumentException($"Invalid extension `{extension}` from `{extensions}`", nameof(extensions)); - } + case "common": + break; + case "advanced": + pipeline.UseAdvancedExtensions(); + break; + case "pipetables": + pipeline.UsePipeTables(); + break; + case "gfm-pipetables": + pipeline.UsePipeTables(new PipeTableOptions { UseHeaderForColumnCount = true }); + break; + case "emphasisextras": + pipeline.UseEmphasisExtras(); + break; + case "listextras": + pipeline.UseListExtras(); + break; + case "hardlinebreak": + pipeline.UseSoftlineBreakAsHardlineBreak(); + break; + case "footnotes": + pipeline.UseFootnotes(); + break; + case "footers": + pipeline.UseFooters(); + break; + case "citations": + pipeline.UseCitations(); + break; + case "attributes": + pipeline.UseGenericAttributes(); + break; + case "gridtables": + pipeline.UseGridTables(); + break; + case "abbreviations": + pipeline.UseAbbreviations(); + break; + case "emojis": + pipeline.UseEmojiAndSmiley(); + break; + case "definitionlists": + pipeline.UseDefinitionLists(); + break; + case "customcontainers": + pipeline.UseCustomContainers(); + break; + case "figures": + pipeline.UseFigures(); + break; + case "mathematics": + pipeline.UseMathematics(); + break; + case "bootstrap": + pipeline.UseBootstrap(); + break; + case "medialinks": + pipeline.UseMediaLinks(); + break; + case "smartypants": + pipeline.UseSmartyPants(); + break; + case "autoidentifiers": + pipeline.UseAutoIdentifiers(); + break; + case "tasklists": + pipeline.UseTaskLists(); + break; + case "diagrams": + pipeline.UseDiagrams(); + break; + case "nofollowlinks": + pipeline.UseReferralLinks("nofollow"); + break; + case "noopenerlinks": + pipeline.UseReferralLinks("noopener"); + break; + case "noreferrerlinks": + pipeline.UseReferralLinks("noreferrer"); + break; + case "nohtml": + pipeline.DisableHtml(); + break; + case "yaml": + pipeline.UseYamlFrontMatter(); + break; + case "nonascii-noescape": + pipeline.UseNonAsciiNoEscape(); + break; + case "autolinks": + pipeline.UseAutoLinks(); + break; + case "globalization": + pipeline.UseGlobalization(); + break; + default: + throw new ArgumentException($"Invalid extension `{extension}` from `{extensions}`", nameof(extensions)); } - return pipeline; } + return pipeline; + } - /// - /// Configures the string to be used for line-endings, when writing. - /// - /// The pipeline. - /// The string to be used for line-endings. - /// The modified pipeline - public static MarkdownPipelineBuilder ConfigureNewLine(this MarkdownPipelineBuilder pipeline, string newLine) - { - pipeline.Use(new ConfigureNewLineExtension(newLine)); - return pipeline; - } + /// + /// Configures the string to be used for line-endings, when writing. + /// + /// The pipeline. + /// The string to be used for line-endings. + /// The modified pipeline + public static MarkdownPipelineBuilder ConfigureNewLine(this MarkdownPipelineBuilder pipeline, string newLine) + { + pipeline.Use(new ConfigureNewLineExtension(newLine)); + return pipeline; + } - /// - /// Disables parsing of ATX and Setex headings - /// - /// The pipeline. - /// The modified pipeline - public static MarkdownPipelineBuilder DisableHeadings(this MarkdownPipelineBuilder pipeline) + /// + /// Disables parsing of ATX and Setex headings + /// + /// The pipeline. + /// The modified pipeline + public static MarkdownPipelineBuilder DisableHeadings(this MarkdownPipelineBuilder pipeline) + { + pipeline.BlockParsers.TryRemove(); + if (pipeline.BlockParsers.TryFind(out var parser)) { - pipeline.BlockParsers.TryRemove(); - if (pipeline.BlockParsers.TryFind(out var parser)) - { - parser.ParseSetexHeadings = false; - } - return pipeline; + parser.ParseSetexHeadings = false; } + return pipeline; + } - /// - /// Enables parsing and tracking of trivia characters - /// - /// The pipeline. - /// he modified pipeline - public static MarkdownPipelineBuilder EnableTrackTrivia(this MarkdownPipelineBuilder pipeline) + /// + /// Enables parsing and tracking of trivia characters + /// + /// The pipeline. + /// he modified pipeline + public static MarkdownPipelineBuilder EnableTrackTrivia(this MarkdownPipelineBuilder pipeline) + { + pipeline.TrackTrivia = true; + if (pipeline.BlockParsers.TryFind(out var parser)) { - pipeline.TrackTrivia = true; - if (pipeline.BlockParsers.TryFind(out var parser)) - { - parser.InfoParser = FencedCodeBlockParser.RoundtripInfoParser; - } - return pipeline; + parser.InfoParser = FencedCodeBlockParser.RoundtripInfoParser; } + return pipeline; } } diff --git a/src/Markdig/MarkdownParserContext.cs b/src/Markdig/MarkdownParserContext.cs index 6cf0020f9..3143c2787 100644 --- a/src/Markdig/MarkdownParserContext.cs +++ b/src/Markdig/MarkdownParserContext.cs @@ -2,26 +2,23 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using System.Collections.Generic; +namespace Markdig; -namespace Markdig +/// +/// Provides a context that can be used as part of parsing Markdown documents. +/// +public class MarkdownParserContext { /// - /// Provides a context that can be used as part of parsing Markdown documents. + /// Gets or sets the context property collection. /// - public class MarkdownParserContext - { - /// - /// Gets or sets the context property collection. - /// - public Dictionary Properties { get; } + public Dictionary Properties { get; } - /// - /// Initializes a new instance of the class. - /// - public MarkdownParserContext() - { - Properties = new Dictionary(); - } + /// + /// Initializes a new instance of the class. + /// + public MarkdownParserContext() + { + Properties = new Dictionary(); } } diff --git a/src/Markdig/MarkdownPipeline.cs b/src/Markdig/MarkdownPipeline.cs index f6e5ad64c..995d72c54 100644 --- a/src/Markdig/MarkdownPipeline.cs +++ b/src/Markdig/MarkdownPipeline.cs @@ -2,147 +2,146 @@ // 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.IO; + using Markdig.Extensions.SelfPipeline; using Markdig.Helpers; using Markdig.Parsers; using Markdig.Renderers; -namespace Markdig +namespace Markdig; + +/// +/// This class is the Markdown pipeline build from a . +/// An instance of is immutable, thread-safe, and should be reused when parsing multiple inputs. +/// +public sealed class MarkdownPipeline { /// - /// This class is the Markdown pipeline build from a . - /// An instance of is immutable, thread-safe, and should be reused when parsing multiple inputs. + /// Initializes a new instance of the class. /// - public sealed class MarkdownPipeline + internal MarkdownPipeline( + OrderedList extensions, + BlockParserList blockParsers, + InlineParserList inlineParsers, + TextWriter? debugLog, + ProcessDocumentDelegate? documentProcessed) { - /// - /// Initializes a new instance of the class. - /// - internal MarkdownPipeline( - OrderedList extensions, - BlockParserList blockParsers, - InlineParserList inlineParsers, - TextWriter? debugLog, - ProcessDocumentDelegate? documentProcessed) - { - if (blockParsers is null) ThrowHelper.ArgumentNullException(nameof(blockParsers)); - if (inlineParsers is null) ThrowHelper.ArgumentNullException(nameof(inlineParsers)); - // Add all default parsers - Extensions = extensions; - BlockParsers = blockParsers; - InlineParsers = inlineParsers; - DebugLog = debugLog; - DocumentProcessed = documentProcessed; - - SelfPipeline = Extensions.Find(); - } + if (blockParsers is null) ThrowHelper.ArgumentNullException(nameof(blockParsers)); + if (inlineParsers is null) ThrowHelper.ArgumentNullException(nameof(inlineParsers)); + // Add all default parsers + Extensions = extensions; + BlockParsers = blockParsers; + InlineParsers = inlineParsers; + DebugLog = debugLog; + DocumentProcessed = documentProcessed; + + SelfPipeline = Extensions.Find(); + } - internal bool PreciseSourceLocation { get; set; } + internal bool PreciseSourceLocation { get; set; } - /// - /// The read-only list of extensions used to build this pipeline. - /// - public OrderedList Extensions { get; } + /// + /// The read-only list of extensions used to build this pipeline. + /// + public OrderedList Extensions { get; } - internal BlockParserList BlockParsers { get; } + internal BlockParserList BlockParsers { get; } - internal InlineParserList InlineParsers { get; } + internal InlineParserList InlineParsers { get; } - // TODO: Move the log to a better place - internal TextWriter? DebugLog { get; } + // TODO: Move the log to a better place + internal TextWriter? DebugLog { get; } - internal ProcessDocumentDelegate? DocumentProcessed; + internal ProcessDocumentDelegate? DocumentProcessed; - internal SelfPipelineExtension? SelfPipeline; + internal SelfPipelineExtension? SelfPipeline; - /// - /// True to parse trivia such as whitespace, extra heading characters and unescaped - /// string values. - /// - public bool TrackTrivia { get; internal set; } + /// + /// True to parse trivia such as whitespace, extra heading characters and unescaped + /// string values. + /// + public bool TrackTrivia { get; internal set; } - /// - /// Allows to setup a . - /// - /// The markdown renderer to setup - public void Setup(IMarkdownRenderer renderer) + /// + /// Allows to setup a . + /// + /// The markdown renderer to setup + public void Setup(IMarkdownRenderer renderer) + { + if (renderer is null) ThrowHelper.ArgumentNullException(nameof(renderer)); + foreach (var extension in Extensions) { - if (renderer is null) ThrowHelper.ArgumentNullException(nameof(renderer)); - foreach (var extension in Extensions) - { - extension.Setup(this, renderer); - } + extension.Setup(this, renderer); } + } - private HtmlRendererCache? _rendererCache; - private HtmlRendererCache? _rendererCacheForCustomWriter; + private HtmlRendererCache? _rendererCache; + private HtmlRendererCache? _rendererCacheForCustomWriter; - internal RentedHtmlRenderer RentHtmlRenderer(TextWriter? writer = null) + internal RentedHtmlRenderer RentHtmlRenderer(TextWriter? writer = null) + { + HtmlRendererCache cache = writer is null + ? _rendererCache ??= new HtmlRendererCache(this, customWriter: false) + : _rendererCacheForCustomWriter ??= new HtmlRendererCache(this, customWriter: true); + + HtmlRenderer renderer = cache.Get(); + + if (writer is not null) { - HtmlRendererCache cache = writer is null - ? _rendererCache ??= new HtmlRendererCache(this, customWriter: false) - : _rendererCacheForCustomWriter ??= new HtmlRendererCache(this, customWriter: true); + renderer.Writer = writer; + } - HtmlRenderer renderer = cache.Get(); + return new RentedHtmlRenderer(cache, renderer); + } - if (writer is not null) - { - renderer.Writer = writer; - } + internal sealed class HtmlRendererCache : ObjectCache + { + private static readonly TextWriter s_dummyWriter = new FastStringWriter(); - return new RentedHtmlRenderer(cache, renderer); + private readonly MarkdownPipeline _pipeline; + private readonly bool _customWriter; + + public HtmlRendererCache(MarkdownPipeline pipeline, bool customWriter = false) + { + _pipeline = pipeline; + _customWriter = customWriter; } - internal sealed class HtmlRendererCache : ObjectCache + protected override HtmlRenderer NewInstance() { - private static readonly TextWriter s_dummyWriter = new FastStringWriter(); + TextWriter writer = _customWriter ? s_dummyWriter : new FastStringWriter(); + var renderer = new HtmlRenderer(writer); + _pipeline.Setup(renderer); + return renderer; + } - private readonly MarkdownPipeline _pipeline; - private readonly bool _customWriter; + protected override void Reset(HtmlRenderer instance) + { + instance.ResetInternal(); - public HtmlRendererCache(MarkdownPipeline pipeline, bool customWriter = false) + if (_customWriter) { - _pipeline = pipeline; - _customWriter = customWriter; + instance.Writer = s_dummyWriter; } - - protected override HtmlRenderer NewInstance() + else { - TextWriter writer = _customWriter ? s_dummyWriter : new FastStringWriter(); - var renderer = new HtmlRenderer(writer); - _pipeline.Setup(renderer); - return renderer; - } - - protected override void Reset(HtmlRenderer instance) - { - instance.ResetInternal(); - - if (_customWriter) - { - instance.Writer = s_dummyWriter; - } - else - { - ((FastStringWriter)instance.Writer).Reset(); - } + ((FastStringWriter)instance.Writer).Reset(); } } + } - internal readonly struct RentedHtmlRenderer : IDisposable - { - private readonly HtmlRendererCache _cache; - public readonly HtmlRenderer Instance; - - internal RentedHtmlRenderer(HtmlRendererCache cache, HtmlRenderer renderer) - { - _cache = cache; - Instance = renderer; - } + internal readonly struct RentedHtmlRenderer : IDisposable + { + private readonly HtmlRendererCache _cache; + public readonly HtmlRenderer Instance; - public void Dispose() => _cache.Release(Instance); + internal RentedHtmlRenderer(HtmlRendererCache cache, HtmlRenderer renderer) + { + _cache = cache; + Instance = renderer; } + + public void Dispose() => _cache.Release(Instance); } } \ No newline at end of file diff --git a/src/Markdig/MarkdownPipelineBuilder.cs b/src/Markdig/MarkdownPipelineBuilder.cs index 7bfd75d5f..8f527f682 100644 --- a/src/Markdig/MarkdownPipelineBuilder.cs +++ b/src/Markdig/MarkdownPipelineBuilder.cs @@ -2,130 +2,129 @@ // 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.IO; + using Markdig.Helpers; using Markdig.Parsers; using Markdig.Parsers.Inlines; -namespace Markdig +namespace Markdig; + +/// +/// This class allows to modify the pipeline to parse and render a Markdown document. +/// +/// NOTE: A pipeline is not thread-safe. +public class MarkdownPipelineBuilder { + private MarkdownPipeline? pipeline; + /// - /// This class allows to modify the pipeline to parse and render a Markdown document. + /// Initializes a new instance of the class. /// - /// NOTE: A pipeline is not thread-safe. - public class MarkdownPipelineBuilder + public MarkdownPipelineBuilder() { - private MarkdownPipeline? pipeline; - - /// - /// Initializes a new instance of the class. - /// - public MarkdownPipelineBuilder() + // Add all default parsers + BlockParsers = new OrderedList() { - // Add all default parsers - BlockParsers = new OrderedList() - { - new ThematicBreakParser(), - new HeadingBlockParser(), - new QuoteBlockParser(), - new ListBlockParser(), - - new HtmlBlockParser(), - new FencedCodeBlockParser(), - new IndentedCodeBlockParser(), - new ParagraphBlockParser(), - }; - - InlineParsers = new OrderedList() - { - new HtmlEntityParser(), - new LinkInlineParser(), - new EscapeInlineParser(), - new EmphasisInlineParser(), - new CodeInlineParser(), - new AutolinkInlineParser(), - new LineBreakInlineParser(), - }; - - Extensions = new OrderedList(); - } + new ThematicBreakParser(), + new HeadingBlockParser(), + new QuoteBlockParser(), + new ListBlockParser(), + + new HtmlBlockParser(), + new FencedCodeBlockParser(), + new IndentedCodeBlockParser(), + new ParagraphBlockParser(), + }; + + InlineParsers = new OrderedList() + { + new HtmlEntityParser(), + new LinkInlineParser(), + new EscapeInlineParser(), + new EmphasisInlineParser(), + new CodeInlineParser(), + new AutolinkInlineParser(), + new LineBreakInlineParser(), + }; + + Extensions = new OrderedList(); + } + + /// + /// Gets the block parsers. + /// + public OrderedList BlockParsers { get; private set; } + + /// + /// Gets the inline parsers. + /// + public OrderedList InlineParsers { get; private set; } + + /// + /// Gets the register extensions. + /// + public OrderedList Extensions { get; } + + /// + /// Gets or sets a value indicating whether to enable precise source location (slower parsing but accurate position for block and inline elements) + /// + public bool PreciseSourceLocation { get; set; } + + /// + /// Gets or sets the debug log. + /// + public TextWriter? DebugLog { get; set; } + + /// + /// True to parse trivia such as whitespace, extra heading characters and unescaped + /// string values. + /// + public bool TrackTrivia { get; internal set; } - /// - /// Gets the block parsers. - /// - public OrderedList BlockParsers { get; private set; } - - /// - /// Gets the inline parsers. - /// - public OrderedList InlineParsers { get; private set; } - - /// - /// Gets the register extensions. - /// - public OrderedList Extensions { get; } - - /// - /// Gets or sets a value indicating whether to enable precise source location (slower parsing but accurate position for block and inline elements) - /// - public bool PreciseSourceLocation { get; set; } - - /// - /// Gets or sets the debug log. - /// - public TextWriter? DebugLog { get; set; } - - /// - /// True to parse trivia such as whitespace, extra heading characters and unescaped - /// string values. - /// - public bool TrackTrivia { get; internal set; } - - /// - /// Occurs when a document has been processed after the method. - /// - public event ProcessDocumentDelegate? DocumentProcessed; - - internal ProcessDocumentDelegate? GetDocumentProcessed => DocumentProcessed; - - /// - /// Builds a pipeline from this instance. Once the pipeline is build, it cannot be modified. - /// - /// An extension cannot be null - public MarkdownPipeline Build() + /// + /// Occurs when a document has been processed after the method. + /// + public event ProcessDocumentDelegate? DocumentProcessed; + + internal ProcessDocumentDelegate? GetDocumentProcessed => DocumentProcessed; + + /// + /// Builds a pipeline from this instance. Once the pipeline is build, it cannot be modified. + /// + /// An extension cannot be null + public MarkdownPipeline Build() + { + if (pipeline != null) { - if (pipeline != null) - { - return pipeline; - } + return pipeline; + } - // TODO: Review the whole initialization process for extensions - // - It does not prevent a user to modify the pipeline after it has been used - // - a pipeline is not thread safe. - // We should find a proper way to make the pipeline safely modifiable/freezable (PipelineBuilder -> Pipeline) + // TODO: Review the whole initialization process for extensions + // - It does not prevent a user to modify the pipeline after it has been used + // - a pipeline is not thread safe. + // We should find a proper way to make the pipeline safely modifiable/freezable (PipelineBuilder -> Pipeline) - // Allow extensions to modify existing BlockParsers, InlineParsers and Renderer - foreach (var extension in Extensions) + // Allow extensions to modify existing BlockParsers, InlineParsers and Renderer + foreach (var extension in Extensions) + { + if (extension is null) { - if (extension is null) - { - ThrowHelper.InvalidOperationException("An extension cannot be null"); - } - extension.Setup(this); + ThrowHelper.InvalidOperationException("An extension cannot be null"); } - - pipeline = new MarkdownPipeline( - new OrderedList(Extensions), - new BlockParserList(BlockParsers), - new InlineParserList(InlineParsers), - DebugLog, - GetDocumentProcessed) - { - PreciseSourceLocation = PreciseSourceLocation, - TrackTrivia = TrackTrivia, - }; - return pipeline; + extension.Setup(this); } + + pipeline = new MarkdownPipeline( + new OrderedList(Extensions), + new BlockParserList(BlockParsers), + new InlineParserList(InlineParsers), + DebugLog, + GetDocumentProcessed) + { + PreciseSourceLocation = PreciseSourceLocation, + TrackTrivia = TrackTrivia, + }; + return pipeline; } } \ No newline at end of file diff --git a/src/Markdig/Parsers/BlockParser.cs b/src/Markdig/Parsers/BlockParser.cs index b6aa324a8..64238f69f 100644 --- a/src/Markdig/Parsers/BlockParser.cs +++ b/src/Markdig/Parsers/BlockParser.cs @@ -4,88 +4,87 @@ using Markdig.Syntax; -namespace Markdig.Parsers -{ - /// - /// Delegates called when processing a block - /// - public delegate void ProcessBlockDelegate(BlockProcessor processor, Block block); +namespace Markdig.Parsers; + +/// +/// Delegates called when processing a block +/// +public delegate void ProcessBlockDelegate(BlockProcessor processor, Block block); +/// +/// Base class for a parser of a +/// +/// +public abstract class BlockParser : ParserBase, IBlockParser +{ /// - /// Base class for a parser of a + /// Determines whether the specified char is an opening character. /// - /// - public abstract class BlockParser : ParserBase, IBlockParser + /// The character. + /// true if the specified char is an opening character. + public bool HasOpeningCharacter(char c) { - /// - /// Determines whether the specified char is an opening character. - /// - /// The character. - /// true if the specified char is an opening character. - public bool HasOpeningCharacter(char c) + if (OpeningCharacters != null) { - if (OpeningCharacters != null) + foreach (char openingChar in OpeningCharacters) { - foreach (char openingChar in OpeningCharacters) + if (openingChar == c) { - if (openingChar == c) - { - return true; - } + return true; } } - return false; } + return false; + } - // TODO: Add comment - public event ProcessBlockDelegate? Closed; + // TODO: Add comment + public event ProcessBlockDelegate? Closed; - internal ProcessBlockDelegate? GetClosedEvent => Closed; + internal ProcessBlockDelegate? GetClosedEvent => Closed; - /// - /// Determines whether this instance can interrupt the specified block being processed. - /// - /// The parser processor. - /// The block being processed. - /// true if this parser can interrupt the specified block being processed. - public virtual bool CanInterrupt(BlockProcessor processor, Block block) - { - // By default, all blocks can interrupt a ParagraphBlock except: - // - Setext heading - // - Indented code block - // - HTML blocks - return true; - } + /// + /// Determines whether this instance can interrupt the specified block being processed. + /// + /// The parser processor. + /// The block being processed. + /// true if this parser can interrupt the specified block being processed. + public virtual bool CanInterrupt(BlockProcessor processor, Block block) + { + // By default, all blocks can interrupt a ParagraphBlock except: + // - Setext heading + // - Indented code block + // - HTML blocks + return true; + } - /// - /// Tries to match a block opening. - /// - /// The parser processor. - /// The result of the match - public abstract BlockState TryOpen(BlockProcessor processor); + /// + /// Tries to match a block opening. + /// + /// The parser processor. + /// The result of the match + public abstract BlockState TryOpen(BlockProcessor processor); - /// - /// Tries to continue matching a block already opened. - /// - /// The parser processor. - /// The block already opened. - /// The result of the match. By default, don't expect any newline - public virtual BlockState TryContinue(BlockProcessor processor, Block block) - { - // By default we don't expect any newline - return BlockState.None; - } + /// + /// Tries to continue matching a block already opened. + /// + /// The parser processor. + /// The block already opened. + /// The result of the match. By default, don't expect any newline + public virtual BlockState TryContinue(BlockProcessor processor, Block block) + { + // By default we don't expect any newline + return BlockState.None; + } - /// - /// Called when a block matched by this parser is being closed (to allow final computation on the block). - /// - /// The parser processor. - /// The block being closed. - /// true to keep the block; false to remove it. True by default. - public virtual bool Close(BlockProcessor processor, Block block) - { - // By default keep the block - return true; - } + /// + /// Called when a block matched by this parser is being closed (to allow final computation on the block). + /// + /// The parser processor. + /// The block being closed. + /// true to keep the block; false to remove it. True by default. + public virtual bool Close(BlockProcessor processor, Block block) + { + // By default keep the block + return true; } } \ No newline at end of file diff --git a/src/Markdig/Parsers/BlockParserList.cs b/src/Markdig/Parsers/BlockParserList.cs index eefd31ce0..c1d2ec5e9 100644 --- a/src/Markdig/Parsers/BlockParserList.cs +++ b/src/Markdig/Parsers/BlockParserList.cs @@ -2,22 +2,19 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using System.Collections.Generic; +namespace Markdig.Parsers; -namespace Markdig.Parsers +/// +/// A List of . +/// +/// +public class BlockParserList : ParserList { /// - /// A List of . + /// Initializes a new instance of the class. /// - /// - public class BlockParserList : ParserList + /// The parsers. + public BlockParserList(IEnumerable parsers) : base(parsers) { - /// - /// Initializes a new instance of the class. - /// - /// The parsers. - public BlockParserList(IEnumerable parsers) : base(parsers) - { - } } } \ No newline at end of file diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index d5e78c33f..fe0493d3f 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -2,1038 +2,1036 @@ // 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.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; + using Markdig.Helpers; using Markdig.Syntax; -namespace Markdig.Parsers +namespace Markdig.Parsers; + +/// +/// The block processor. +/// +public class BlockProcessor { + private int currentStackIndex; + private int originalLineStart; + /// - /// The block processor. + /// Initializes a new instance of the class. /// - public class BlockProcessor + /// The document to build blocks into. + /// The list of parsers. + /// A parser context used for the parsing. + /// Whether to parse trivia such as whitespace, extra heading characters and unescaped string values. + /// + /// + public BlockProcessor(MarkdownDocument document, BlockParserList parsers, MarkdownParserContext? context, bool trackTrivia = false) { - private int currentStackIndex; - private int originalLineStart; - - /// - /// Initializes a new instance of the class. - /// - /// The document to build blocks into. - /// The list of parsers. - /// A parser context used for the parsing. - /// Whether to parse trivia such as whitespace, extra heading characters and unescaped string values. - /// - /// - public BlockProcessor(MarkdownDocument document, BlockParserList parsers, MarkdownParserContext? context, bool trackTrivia = false) - { - Setup(document, parsers, context, trackTrivia); + Setup(document, parsers, context, trackTrivia); - document.IsOpen = true; - Open(document); - } + document.IsOpen = true; + Open(document); + } - private BlockProcessor() { } + private BlockProcessor() { } - public bool SkipFirstUnwindSpace { get; set; } + public bool SkipFirstUnwindSpace { get; set; } - /// - /// Gets the new blocks to push. A is required to push new blocks that it creates to this property. - /// - public Stack NewBlocks { get; } = new(); + /// + /// Gets the new blocks to push. A is required to push new blocks that it creates to this property. + /// + public Stack NewBlocks { get; } = new(); - /// - /// Gets the list of s configured with this parser state. - /// - public BlockParserList Parsers { get; private set; } = null!; // Set in Setup + /// + /// Gets the list of s configured with this parser state. + /// + public BlockParserList Parsers { get; private set; } = null!; // Set in Setup - /// - /// Gets the parser context or null if none is available. - /// - public MarkdownParserContext? Context { get; private set; } + /// + /// Gets the parser context or null if none is available. + /// + public MarkdownParserContext? Context { get; private set; } - /// - /// Gets the current active container. - /// - public ContainerBlock? CurrentContainer { get; private set; } + /// + /// Gets the current active container. + /// + public ContainerBlock? CurrentContainer { get; private set; } - /// - /// Gets the last block that is opened. - /// - public Block? CurrentBlock { get; private set; } + /// + /// Gets the last block that is opened. + /// + public Block? CurrentBlock { get; private set; } - /// - /// Gets the last block that is created. - /// - public Block? LastBlock { get; private set; } + /// + /// Gets the last block that is created. + /// + public Block? LastBlock { get; private set; } - /// - /// Gets the next block in a . - /// - public Block? NextContinue + /// + /// Gets the next block in a . + /// + public Block? NextContinue + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - int index = currentStackIndex + 1; - return index < OpenedBlocks.Count ? OpenedBlocks[index].Block : null; - } + int index = currentStackIndex + 1; + return index < OpenedBlocks.Count ? OpenedBlocks[index].Block : null; } + } + + /// + /// Gets the root document. + /// + public MarkdownDocument Document { get; private set; } = null!; // Set in Setup + + /// + /// The current line being processed. + /// + public StringSlice Line; + + /// + /// Gets or sets the current line start position. + /// + public int CurrentLineStartPosition { get; private set; } + + /// + /// Gets the index of the line in the source text. + /// + public int LineIndex { get; set; } + + /// + /// Gets a value indicating whether the line is blank (valid only after has been called). + /// + public bool IsBlankLine => Line.IsEmpty; + + /// + /// Gets the current character being processed. + /// + public char CurrentChar => Line.CurrentChar; + + /// + /// Gets or sets the column. + /// + public int Column { get; set; } + + /// + /// Gets the position of the current character in the line being processed. + /// + public int Start => Line.Start; - /// - /// Gets the root document. - /// - public MarkdownDocument Document { get; private set; } = null!; // Set in Setup - - /// - /// The current line being processed. - /// - public StringSlice Line; - - /// - /// Gets or sets the current line start position. - /// - public int CurrentLineStartPosition { get; private set; } - - /// - /// Gets the index of the line in the source text. - /// - public int LineIndex { get; set; } - - /// - /// Gets a value indicating whether the line is blank (valid only after has been called). - /// - public bool IsBlankLine => Line.IsEmpty; - - /// - /// Gets the current character being processed. - /// - public char CurrentChar => Line.CurrentChar; - - /// - /// Gets or sets the column. - /// - public int Column { get; set; } - - /// - /// Gets the position of the current character in the line being processed. - /// - public int Start => Line.Start; - - /// - /// Gets the current indent position (number of columns between the previous indent and the current position). - /// - public int Indent => Column - ColumnBeforeIndent; - - /// - /// Gets a value indicating whether a code indentation is at the beginning of the line being processed. - /// - public bool IsCodeIndent => Indent >= 4; - - /// - /// Gets the column position before the indent occurred. - /// - public int ColumnBeforeIndent { get; private set; } - - /// - /// Gets the character position before the indent occurred. - /// - public int StartBeforeIndent { get; private set; } - - /// - /// Gets a boolean indicating whether the current line being parsed is lazy continuation. - /// - public bool IsLazy { get; private set; } - - /// - /// Gets the current stack of being processed. - /// - private List OpenedBlocks { get; } = new(); - - private bool ContinueProcessingLine { get; set; } - - /// - /// Gets or sets the position of the first character trivia is encountered - /// and not yet assigned to a syntax node. - /// Trivia: only used when is enabled, otherwise 0. - /// - public int TriviaStart { get; set; } - - /// - /// Returns trivia that has not yet been assigned to any node and - /// advances the position of trivia to the ending position. - /// - /// End position of the trivia - /// - public StringSlice UseTrivia(int end) + /// + /// Gets the current indent position (number of columns between the previous indent and the current position). + /// + public int Indent => Column - ColumnBeforeIndent; + + /// + /// Gets a value indicating whether a code indentation is at the beginning of the line being processed. + /// + public bool IsCodeIndent => Indent >= 4; + + /// + /// Gets the column position before the indent occurred. + /// + public int ColumnBeforeIndent { get; private set; } + + /// + /// Gets the character position before the indent occurred. + /// + public int StartBeforeIndent { get; private set; } + + /// + /// Gets a boolean indicating whether the current line being parsed is lazy continuation. + /// + public bool IsLazy { get; private set; } + + /// + /// Gets the current stack of being processed. + /// + private List OpenedBlocks { get; } = new(); + + private bool ContinueProcessingLine { get; set; } + + /// + /// Gets or sets the position of the first character trivia is encountered + /// and not yet assigned to a syntax node. + /// Trivia: only used when is enabled, otherwise 0. + /// + public int TriviaStart { get; set; } + + /// + /// Returns trivia that has not yet been assigned to any node and + /// advances the position of trivia to the ending position. + /// + /// End position of the trivia + /// + public StringSlice UseTrivia(int end) + { + var stringSlice = new StringSlice(Line.Text, TriviaStart, end); + TriviaStart = end + 1; + return stringSlice; + } + + /// + /// Returns the current stack of to assign it to a . + /// Afterwards, the is set to null. + /// + internal List UseLinesBefore() + { + var linesBefore = LinesBefore; + LinesBefore = null; + return linesBefore!; + } + + /// + /// Gets or sets the stack of empty lines not yet assigned to any . + /// An entry may contain an empty . In that case the + /// is relevant. Otherwise, the + /// entry will contain trivia. + /// + public List? LinesBefore { get; set; } + + /// + /// True to parse trivia such as whitespace, extra heading characters and unescaped + /// string values. + /// + public bool TrackTrivia { get; private set; } + + /// + /// Get the current Container that is currently opened + /// + /// The current Container that is currently opened + public ContainerBlock GetCurrentContainerOpened() + { + var container = CurrentContainer; + while (container != null && !container.IsOpen) { - var stringSlice = new StringSlice(Line.Text, TriviaStart, end); - TriviaStart = end + 1; - return stringSlice; + container = container.Parent; } - /// - /// Returns the current stack of to assign it to a . - /// Afterwards, the is set to null. - /// - internal List UseLinesBefore() + return container!; + } + + /// + /// Returns the next character in the line being processed. Update and . + /// + /// The next character or `\0` if end of line is reached + public char NextChar() + { + var c = Line.CurrentChar; + if (c == '\t') { - var linesBefore = LinesBefore; - LinesBefore = null; - return linesBefore!; + Column = CharHelper.AddTab(Column); } - - /// - /// Gets or sets the stack of empty lines not yet assigned to any . - /// An entry may contain an empty . In that case the - /// is relevant. Otherwise, the - /// entry will contain trivia. - /// - public List? LinesBefore { get; set; } - - /// - /// True to parse trivia such as whitespace, extra heading characters and unescaped - /// string values. - /// - public bool TrackTrivia { get; private set; } - - /// - /// Get the current Container that is currently opened - /// - /// The current Container that is currently opened - public ContainerBlock GetCurrentContainerOpened() + else { - var container = CurrentContainer; - while (container != null && !container.IsOpen) - { - container = container.Parent; - } + Column++; + } + return Line.NextChar(); + } - return container!; + /// + /// Returns the next character in the line taking into space taken by tabs. Update and . + /// + public void NextColumn() + { + var c = Line.CurrentChar; + // If we are across a tab, we should just add 1 column + if (c == '\t' && CharHelper.IsAcrossTab(Column)) + { + Column++; + } + else + { + Line.NextChar(); + Column++; } + } + + /// + /// Peeks a character at the specified offset from the current position in the line. + /// + /// The offset. + /// A character peeked at the specified offset + public char PeekChar(int offset) + { + return Line.PeekChar(offset); + } - /// - /// Returns the next character in the line being processed. Update and . - /// - /// The next character or `\0` if end of line is reached - public char NextChar() + /// + /// Restarts the indent from the current position. + /// + public void RestartIndent() + { + StartBeforeIndent = Start; + ColumnBeforeIndent = Column; + } + + /// + /// Parses the indentation from the current position in the line, updating , + /// , and accordingly + /// taking into account space taken by tabs. + /// + public void ParseIndent() + { + var c = CurrentChar; + var previousStartBeforeIndent = StartBeforeIndent; + var startBeforeIndent = Start; + var previousColumnBeforeIndent = ColumnBeforeIndent; + var columnBeforeIndent = Column; + while (c != '\0') { - var c = Line.CurrentChar; if (c == '\t') { Column = CharHelper.AddTab(Column); } - else - { - Column++; - } - return Line.NextChar(); - } - - /// - /// Returns the next character in the line taking into space taken by tabs. Update and . - /// - public void NextColumn() - { - var c = Line.CurrentChar; - // If we are across a tab, we should just add 1 column - if (c == '\t' && CharHelper.IsAcrossTab(Column)) + else if (c == ' ') { Column++; } else { - Line.NextChar(); - Column++; + break; } + c = Line.NextChar(); } - - /// - /// Peeks a character at the specified offset from the current position in the line. - /// - /// The offset. - /// A character peeked at the specified offset - public char PeekChar(int offset) + if (columnBeforeIndent == Column) { - return Line.PeekChar(offset); + StartBeforeIndent = previousStartBeforeIndent; + ColumnBeforeIndent = previousColumnBeforeIndent; } - - /// - /// Restarts the indent from the current position. - /// - public void RestartIndent() + else { - StartBeforeIndent = Start; - ColumnBeforeIndent = Column; + StartBeforeIndent = startBeforeIndent; + ColumnBeforeIndent = columnBeforeIndent; } + } - /// - /// Parses the indentation from the current position in the line, updating , - /// , and accordingly - /// taking into account space taken by tabs. - /// - public void ParseIndent() + /// + /// Moves to the position to the specified column position, taking into account spaces in tabs. + /// + /// The new column position to move the cursor to. + public void GoToColumn(int newColumn) + { + // Optimized path when we are moving above the previous start of indent + if (newColumn >= ColumnBeforeIndent) { - var c = CurrentChar; - var previousStartBeforeIndent = StartBeforeIndent; - var startBeforeIndent = Start; - var previousColumnBeforeIndent = ColumnBeforeIndent; - var columnBeforeIndent = Column; - while (c != '\0') - { - if (c == '\t') - { - Column = CharHelper.AddTab(Column); - } - else if (c == ' ') - { - Column++; - } - else - { - break; - } - c = Line.NextChar(); - } - if (columnBeforeIndent == Column) - { - StartBeforeIndent = previousStartBeforeIndent; - ColumnBeforeIndent = previousColumnBeforeIndent; - } - else - { - StartBeforeIndent = startBeforeIndent; - ColumnBeforeIndent = columnBeforeIndent; - } + Line.Start = StartBeforeIndent; + Column = ColumnBeforeIndent; } - - /// - /// Moves to the position to the specified column position, taking into account spaces in tabs. - /// - /// The new column position to move the cursor to. - public void GoToColumn(int newColumn) + else + { + Line.Start = originalLineStart; + Column = 0; + ColumnBeforeIndent = 0; + StartBeforeIndent = originalLineStart; + } + for (; Line.Start <= Line.End && Column < newColumn; Line.Start++) { - // Optimized path when we are moving above the previous start of indent - if (newColumn >= ColumnBeforeIndent) + var c = Line.Text[Line.Start]; + if (c == '\t') { - Line.Start = StartBeforeIndent; - Column = ColumnBeforeIndent; + Column = CharHelper.AddTab(Column); } else { - Line.Start = originalLineStart; - Column = 0; - ColumnBeforeIndent = 0; - StartBeforeIndent = originalLineStart; - } - for (; Line.Start <= Line.End && Column < newColumn; Line.Start++) - { - var c = Line.Text[Line.Start]; - if (c == '\t') + if (!c.IsSpaceOrTab()) { - Column = CharHelper.AddTab(Column); + ColumnBeforeIndent = Column + 1; + StartBeforeIndent = Line.Start + 1; } - else - { - if (!c.IsSpaceOrTab()) - { - ColumnBeforeIndent = Column + 1; - StartBeforeIndent = Line.Start + 1; - } - Column++; - } + Column++; } - if (Column > newColumn) + } + if (Column > newColumn) + { + Column = newColumn; + if (Line.Start > 0) { - Column = newColumn; - if (Line.Start > 0) - { - Line.Start--; - } + Line.Start--; } } + } - /// - /// Unwind any previous indent from the current character back to the first space. - /// - public void UnwindAllIndents() + /// + /// Unwind any previous indent from the current character back to the first space. + /// + public void UnwindAllIndents() + { + // Find the previous first space on the current line + var previousStart = Line.Start; + for (; Line.Start > originalLineStart; Line.Start--) { - // Find the previous first space on the current line - var previousStart = Line.Start; - for (; Line.Start > originalLineStart; Line.Start--) - { - var c = Line.PeekCharAbsolute(Line.Start - 1); + var c = Line.PeekCharAbsolute(Line.Start - 1); - // don't unwind all the way next to a '>', but one space right of the '>' if there is a space - if (TrackTrivia && SkipFirstUnwindSpace && Line.Start == TriviaStart) - { - break; - } - if (c == 0) - { - break; - } - if (!c.IsSpaceOrTab()) - { - break; - } + // don't unwind all the way next to a '>', but one space right of the '>' if there is a space + if (TrackTrivia && SkipFirstUnwindSpace && Line.Start == TriviaStart) + { + break; } - var targetStart = Line.Start; - // Nothing changed? Early exit - if (previousStart == targetStart) + if (c == 0) { - return; + break; + } + if (!c.IsSpaceOrTab()) + { + break; } + } + var targetStart = Line.Start; + // Nothing changed? Early exit + if (previousStart == targetStart) + { + return; + } - // TODO: factorize the following code with what is done with GoToColumn + // TODO: factorize the following code with what is done with GoToColumn - // If we have found the first space, we need to recalculate the correct column - Line.Start = originalLineStart; - Column = 0; - ColumnBeforeIndent = 0; - StartBeforeIndent = originalLineStart; + // If we have found the first space, we need to recalculate the correct column + Line.Start = originalLineStart; + Column = 0; + ColumnBeforeIndent = 0; + StartBeforeIndent = originalLineStart; - for (; Line.Start < targetStart; Line.Start++) + for (; Line.Start < targetStart; Line.Start++) + { + var c = Line.Text[Line.Start]; + if (c == '\t') + { + Column = CharHelper.AddTab(Column); + } + else { - var c = Line.Text[Line.Start]; - if (c == '\t') + if (!c.IsSpaceOrTab()) { - Column = CharHelper.AddTab(Column); + ColumnBeforeIndent = Column + 1; + StartBeforeIndent = Line.Start + 1; } - else - { - if (!c.IsSpaceOrTab()) - { - ColumnBeforeIndent = Column + 1; - StartBeforeIndent = Line.Start + 1; - } - Column++; - } + Column++; } - - // Reset the indent - ColumnBeforeIndent = Column; - StartBeforeIndent = Start; } - /// - /// Moves to the position to the code indent ( + 4 spaces). - /// - /// The column offset to apply to this indent. - public void GoToCodeIndent(int columnOffset = 0) - { - GoToColumn(ColumnBeforeIndent + 4 + columnOffset); - } + // Reset the indent + ColumnBeforeIndent = Column; + StartBeforeIndent = Start; + } - /// - /// Opens the specified block. - /// - /// The block. - /// - /// The block must be opened - public void Open(Block block) - { - if (block is null) ThrowHelper.ArgumentNullException(nameof(block)); - if (!block.IsOpen) ThrowHelper.ArgumentException("The block must be opened", nameof(block)); - OpenedBlocks.Add(block); - } + /// + /// Moves to the position to the code indent ( + 4 spaces). + /// + /// The column offset to apply to this indent. + public void GoToCodeIndent(int columnOffset = 0) + { + GoToColumn(ColumnBeforeIndent + 4 + columnOffset); + } - /// - /// Force closing the specified block. - /// - /// The block. - public void Close(Block block) + /// + /// Opens the specified block. + /// + /// The block. + /// + /// The block must be opened + public void Open(Block block) + { + if (block is null) ThrowHelper.ArgumentNullException(nameof(block)); + if (!block.IsOpen) ThrowHelper.ArgumentException("The block must be opened", nameof(block)); + OpenedBlocks.Add(block); + } + + /// + /// Force closing the specified block. + /// + /// The block. + public void Close(Block block) + { + // If we close a block, we close all blocks above + for (int i = OpenedBlocks.Count - 1; i >= 0; i--) { - // If we close a block, we close all blocks above - for (int i = OpenedBlocks.Count - 1; i >= 0; i--) + if (ReferenceEquals(OpenedBlocks[i].Block, block)) { - if (ReferenceEquals(OpenedBlocks[i].Block, block)) + for (int j = OpenedBlocks.Count - 1; j >= i; j--) { - for (int j = OpenedBlocks.Count - 1; j >= i; j--) - { - Close(j); - } - break; + Close(j); } + break; } } + } - /// - /// Discards the specified block from the stack, remove from its parent. - /// - /// The block. - public void Discard(Block block) + /// + /// Discards the specified block from the stack, remove from its parent. + /// + /// The block. + public void Discard(Block block) + { + for (int i = OpenedBlocks.Count - 1; i >= 1; i--) { - for (int i = OpenedBlocks.Count - 1; i >= 1; i--) + if (ReferenceEquals(OpenedBlocks[i].Block, block)) { - if (ReferenceEquals(OpenedBlocks[i].Block, block)) - { - block.Parent!.Remove(block); - OpenedBlocks.RemoveAt(i); - break; - } + block.Parent!.Remove(block); + OpenedBlocks.RemoveAt(i); + break; } } + } - /// - /// Processes a new line. - /// - /// The new line. - public void ProcessLine(StringSlice newLine) - { - CurrentLineStartPosition = newLine.Start; + /// + /// Processes a new line. + /// + /// The new line. + public void ProcessLine(StringSlice newLine) + { + CurrentLineStartPosition = newLine.Start; - Document.LineStartIndexes?.Add(CurrentLineStartPosition); + Document.LineStartIndexes?.Add(CurrentLineStartPosition); - ContinueProcessingLine = true; + ContinueProcessingLine = true; - ResetLine(newLine); + ResetLine(newLine); - TryContinueBlocks(); + TryContinueBlocks(); - // If the line was not entirely processed by pending blocks, try to process it with any new block - TryOpenBlocks(); + // If the line was not entirely processed by pending blocks, try to process it with any new block + TryOpenBlocks(); - // Close blocks that are no longer opened - CloseAll(false); + // Close blocks that are no longer opened + CloseAll(false); - LineIndex++; - } + LineIndex++; + } - internal bool IsOpen(Block block) - { - return OpenedBlocks.Contains(block); - } + internal bool IsOpen(Block block) + { + return OpenedBlocks.Contains(block); + } - /// - /// Closes a block at the specified index. - /// - /// The index. - private void Close(int index) + /// + /// Closes a block at the specified index. + /// + /// The index. + private void Close(int index) + { + var block = OpenedBlocks[index].Block; + // If the pending object is removed, we need to remove it from the parent container + if (block.Parser != null) { - var block = OpenedBlocks[index].Block; - // If the pending object is removed, we need to remove it from the parent container - if (block.Parser != null) + if (!block.Parser.Close(this, block)) { - if (!block.Parser.Close(this, block)) - { - block.Parent?.Remove(block); + block.Parent?.Remove(block); - if (block.IsLeafBlock) - { - Unsafe.As(block).Lines.Release(); - } - } - else + if (block.IsLeafBlock) { - // Invoke the Closed event - var blockClosed = block.Parser.GetClosedEvent; - blockClosed?.Invoke(this, block); + Unsafe.As(block).Lines.Release(); } } - OpenedBlocks.RemoveAt(index); + else + { + // Invoke the Closed event + var blockClosed = block.Parser.GetClosedEvent; + blockClosed?.Invoke(this, block); + } } + OpenedBlocks.RemoveAt(index); + } - /// - /// Closes all the blocks opened. - /// - /// if set to true [force]. - internal void CloseAll(bool force) + /// + /// Closes all the blocks opened. + /// + /// if set to true [force]. + internal void CloseAll(bool force) + { + // Close any previous blocks not opened + for (int i = OpenedBlocks.Count - 1; i >= 1; i--) { - // Close any previous blocks not opened - for (int i = OpenedBlocks.Count - 1; i >= 1; i--) - { - var block = OpenedBlocks[i].Block; + var block = OpenedBlocks[i].Block; - // Stop on the first open block - if (!force && block.IsOpen) - { - break; - } - if (TrackTrivia) + // Stop on the first open block + if (!force && block.IsOpen) + { + break; + } + if (TrackTrivia) + { + if (LinesBefore is { Count: > 0 }) { - if (LinesBefore is { Count: > 0 }) + // single emptylines are significant for the syntax tree, attach + // them to the block + if (LinesBefore.Count == 1) { - // single emptylines are significant for the syntax tree, attach - // them to the block - if (LinesBefore.Count == 1) - { - block.LinesAfter ??= new List(); - var linesBefore = UseLinesBefore(); - block.LinesAfter.AddRange(linesBefore); - } - else - { - // attach multiple lines after to the root most parent ContainerBlock - var rootMostContainerBlock = Block.FindRootMostContainerParent(block); - rootMostContainerBlock.LinesAfter ??= new List(); - var linesBefore = UseLinesBefore(); - rootMostContainerBlock.LinesAfter.AddRange(linesBefore); - } + block.LinesAfter ??= new List(); + var linesBefore = UseLinesBefore(); + block.LinesAfter.AddRange(linesBefore); + } + else + { + // attach multiple lines after to the root most parent ContainerBlock + var rootMostContainerBlock = Block.FindRootMostContainerParent(block); + rootMostContainerBlock.LinesAfter ??= new List(); + var linesBefore = UseLinesBefore(); + rootMostContainerBlock.LinesAfter.AddRange(linesBefore); } } - Close(i); } - UpdateLastBlockAndContainer(); + Close(i); } + UpdateLastBlockAndContainer(); + } - /// - /// Mark all blocks in the stack as opened. - /// - private void OpenAll() + /// + /// Mark all blocks in the stack as opened. + /// + private void OpenAll() + { + for (int i = 1; i < OpenedBlocks.Count; i++) { - for (int i = 1; i < OpenedBlocks.Count; i++) - { - OpenedBlocks[i].Block.IsOpen = true; - } + OpenedBlocks[i].Block.IsOpen = true; } + } + + /// + /// Updates the and . + /// + /// Index of a block in a stack considered as the last block to update from. + private void UpdateLastBlockAndContainer(int stackIndex = -1) + { + List openedBlocks = OpenedBlocks; + currentStackIndex = stackIndex < 0 ? openedBlocks.Count - 1 : stackIndex; - /// - /// Updates the and . - /// - /// Index of a block in a stack considered as the last block to update from. - private void UpdateLastBlockAndContainer(int stackIndex = -1) + Block? currentBlock = null; + for (int i = openedBlocks.Count - 1; i >= 0; i--) { - List openedBlocks = OpenedBlocks; - currentStackIndex = stackIndex < 0 ? openedBlocks.Count - 1 : stackIndex; + var block = openedBlocks[i].Block; + currentBlock ??= block; - Block? currentBlock = null; - for (int i = openedBlocks.Count - 1; i >= 0; i--) + if (block.IsContainerBlock) { - var block = openedBlocks[i].Block; - currentBlock ??= block; - - if (block.IsContainerBlock) - { - var currentContainer = Unsafe.As(block); - CurrentContainer = currentContainer; - LastBlock = currentContainer.LastChild; - CurrentBlock = currentBlock; - return; - } + var currentContainer = Unsafe.As(block); + CurrentContainer = currentContainer; + LastBlock = currentContainer.LastChild; + CurrentBlock = currentBlock; + return; } + } + + CurrentBlock = currentBlock; + LastBlock = null; + } + + /// + /// Tries to continue matching existing opened . + /// + /// + /// A pending parser cannot add a new block when it is not the last pending block + /// or + /// The NewBlocks is not empty. This is happening if a LeafBlock is not the last to be pushed + /// + private void TryContinueBlocks() + { + IsLazy = false; - CurrentBlock = currentBlock; - LastBlock = null; + // Set all blocks non opened. + // They will be marked as open in the following loop + for (int i = 1; i < OpenedBlocks.Count; i++) + { + OpenedBlocks[i].Block.IsOpen = false; } - /// - /// Tries to continue matching existing opened . - /// - /// - /// A pending parser cannot add a new block when it is not the last pending block - /// or - /// The NewBlocks is not empty. This is happening if a LeafBlock is not the last to be pushed - /// - private void TryContinueBlocks() + // Process any current block potentially opened + for (int i = 1; i < OpenedBlocks.Count; i++) { - IsLazy = false; + var block = OpenedBlocks[i].Block; - // Set all blocks non opened. - // They will be marked as open in the following loop - for (int i = 1; i < OpenedBlocks.Count; i++) - { - OpenedBlocks[i].Block.IsOpen = false; - } + ParseIndent(); - // Process any current block potentially opened - for (int i = 1; i < OpenedBlocks.Count; i++) + // If we have a paragraph block, we want to try to match other blocks before trying the Paragraph + if (block.IsParagraphBlock) { - var block = OpenedBlocks[i].Block; - - ParseIndent(); - - // If we have a paragraph block, we want to try to match other blocks before trying the Paragraph - if (block.IsParagraphBlock) - { - break; - } + break; + } - // Else tries to match the Default with the current line - var parser = block.Parser!; + // Else tries to match the Default with the current line + var parser = block.Parser!; - // If we have a discard, we can remove it from the current state - UpdateLastBlockAndContainer(i); - var result = parser.TryContinue(this, block); - if (result == BlockState.Skip) - { - continue; - } + // If we have a discard, we can remove it from the current state + UpdateLastBlockAndContainer(i); + var result = parser.TryContinue(this, block); + if (result == BlockState.Skip) + { + continue; + } - if (result == BlockState.None) - { - break; - } + if (result == BlockState.None) + { + break; + } - RestartIndent(); + RestartIndent(); - // In case the BlockParser has modified the BlockProcessor we are iterating on - if (i >= OpenedBlocks.Count) - { - i = OpenedBlocks.Count - 1; - } + // In case the BlockParser has modified the BlockProcessor we are iterating on + if (i >= OpenedBlocks.Count) + { + i = OpenedBlocks.Count - 1; + } - // If a parser is adding a block, it must be the last of the list - if ((i + 1) < OpenedBlocks.Count && NewBlocks.Count > 0) - { - ThrowHelper.InvalidOperationException("A pending parser cannot add a new block when it is not the last pending block"); - } + // If a parser is adding a block, it must be the last of the list + if ((i + 1) < OpenedBlocks.Count && NewBlocks.Count > 0) + { + ThrowHelper.InvalidOperationException("A pending parser cannot add a new block when it is not the last pending block"); + } - // If we have a leaf block - if (block.IsLeafBlock && NewBlocks.Count == 0) + // If we have a leaf block + if (block.IsLeafBlock && NewBlocks.Count == 0) + { + ContinueProcessingLine = false; + if (!result.IsDiscard()) { - ContinueProcessingLine = false; - if (!result.IsDiscard()) + if (TrackTrivia) { - if (TrackTrivia) + if (block is FencedCodeBlock && block.Parent is ListItemBlock) { - if (block is FencedCodeBlock && block.Parent is ListItemBlock) - { - // the line was already given to the parent, rendering will ignore that parent line. - // The child FencedCodeBlock should get the eaten whitespace at start of the line. - UnwindAllIndents(); - } + // the line was already given to the parent, rendering will ignore that parent line. + // The child FencedCodeBlock should get the eaten whitespace at start of the line. + UnwindAllIndents(); } - - Unsafe.As(block).AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia); } + + Unsafe.As(block).AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia); } + } - // A block is open only if it has a Continue state. - // otherwise it is a Break state, and we don't keep it opened - block.IsOpen = result == BlockState.Continue || result == BlockState.ContinueDiscard; + // A block is open only if it has a Continue state. + // otherwise it is a Break state, and we don't keep it opened + block.IsOpen = result == BlockState.Continue || result == BlockState.ContinueDiscard; - if (result == BlockState.BreakDiscard) + if (result == BlockState.BreakDiscard) + { + if (Line.IsEmpty) { - if (Line.IsEmpty) + if (TrackTrivia) { - if (TrackTrivia) - { - LinesBefore ??= new List(); - var line = new StringSlice(Line.Text, TriviaStart, Line.Start - 1, Line.NewLine); - LinesBefore.Add(line); - Line.Start = StartBeforeIndent; - } + LinesBefore ??= new List(); + var line = new StringSlice(Line.Text, TriviaStart, Line.Start - 1, Line.NewLine); + LinesBefore.Add(line); + Line.Start = StartBeforeIndent; } - ContinueProcessingLine = false; - break; } + ContinueProcessingLine = false; + break; + } - bool isLast = i == OpenedBlocks.Count - 1; - if (ContinueProcessingLine) - { - ProcessNewBlocks(result, false); - } - if (isLast || !ContinueProcessingLine) - { - break; - } + bool isLast = i == OpenedBlocks.Count - 1; + if (ContinueProcessingLine) + { + ProcessNewBlocks(result, false); + } + if (isLast || !ContinueProcessingLine) + { + break; } } + } - /// - /// First phase of the process, try to open new blocks. - /// - private void TryOpenBlocks() + /// + /// First phase of the process, try to open new blocks. + /// + private void TryOpenBlocks() + { + int previousStart = -1; + while (ContinueProcessingLine) { - int previousStart = -1; - while (ContinueProcessingLine) + // Security check so that the parser can't go into a crazy infinite loop if one extension is messing + if (previousStart == Start) { - // Security check so that the parser can't go into a crazy infinite loop if one extension is messing - if (previousStart == Start) - { - ThrowHelper.InvalidOperationException($"The parser is in an invalid infinite loop while trying to parse blocks at line [{LineIndex}] with line [{Line}]"); - } - previousStart = Start; + ThrowHelper.InvalidOperationException($"The parser is in an invalid infinite loop while trying to parse blocks at line [{LineIndex}] with line [{Line}]"); + } + previousStart = Start; - // Eat indent spaces before checking the character - ParseIndent(); + // Eat indent spaces before checking the character + ParseIndent(); - var parsers = Parsers.GetParsersForOpeningCharacter(CurrentChar); - var globalParsers = Parsers.GlobalParsers; + var parsers = Parsers.GetParsersForOpeningCharacter(CurrentChar); + var globalParsers = Parsers.GlobalParsers; - if (parsers != null) + if (parsers != null) + { + if (TryOpenBlocks(parsers)) { - if (TryOpenBlocks(parsers)) - { - RestartIndent(); - continue; - } + RestartIndent(); + continue; } + } - if (globalParsers != null && ContinueProcessingLine) + if (globalParsers != null && ContinueProcessingLine) + { + if (TryOpenBlocks(globalParsers)) { - if (TryOpenBlocks(globalParsers)) - { - RestartIndent(); - continue; - } + RestartIndent(); + continue; } - - break; } + + break; } + } - /// - /// Tries to open new blocks using the specified list of - /// - /// The parsers. - /// true to continue processing the current line - private bool TryOpenBlocks(BlockParser[] parsers) + /// + /// Tries to open new blocks using the specified list of + /// + /// The parsers. + /// true to continue processing the current line + private bool TryOpenBlocks(BlockParser[] parsers) + { + for (int j = 0; j < parsers.Length; j++) { - for (int j = 0; j < parsers.Length; j++) + IsLazy = false; + var blockParser = parsers[j]; + if (Line.IsEmpty) { - IsLazy = false; - var blockParser = parsers[j]; - if (Line.IsEmpty) + if (TrackTrivia) { - if (TrackTrivia) - { - LinesBefore ??= new List(); - var line = new StringSlice(Line.Text, TriviaStart, Line.Start - 1, Line.NewLine); - LinesBefore.Add(line); - Line.Start = StartBeforeIndent; - } - ContinueProcessingLine = false; - break; + LinesBefore ??= new List(); + var line = new StringSlice(Line.Text, TriviaStart, Line.Start - 1, Line.NewLine); + LinesBefore.Add(line); + Line.Start = StartBeforeIndent; } + ContinueProcessingLine = false; + break; + } - // UpdateLastBlockAndContainer the state of CurrentBlock and LastContainer - UpdateLastBlockAndContainer(); + // UpdateLastBlockAndContainer the state of CurrentBlock and LastContainer + UpdateLastBlockAndContainer(); - // If a block parser cannot interrupt a paragraph, and the last block is a paragraph - // we can skip this parser + // If a block parser cannot interrupt a paragraph, and the last block is a paragraph + // we can skip this parser - var lastBlock = CurrentBlock!; - if (!blockParser.CanInterrupt(this, lastBlock)) - { - continue; - } + var lastBlock = CurrentBlock!; + if (!blockParser.CanInterrupt(this, lastBlock)) + { + continue; + } - IsLazy = lastBlock.IsParagraphBlock && blockParser is ParagraphBlockParser; + IsLazy = lastBlock.IsParagraphBlock && blockParser is ParagraphBlockParser; - var result = IsLazy - ? blockParser.TryContinue(this, lastBlock) - : blockParser.TryOpen(this); + var result = IsLazy + ? blockParser.TryContinue(this, lastBlock) + : blockParser.TryOpen(this); - if (result == BlockState.None) + if (result == BlockState.None) + { + // If we have reached a blank line after trying to parse a paragraph + // we can ignore it + if (IsLazy && IsBlankLine) { - // If we have reached a blank line after trying to parse a paragraph - // we can ignore it - if (IsLazy && IsBlankLine) - { - ContinueProcessingLine = false; - break; - } - continue; + ContinueProcessingLine = false; + break; } + continue; + } - // Special case for paragraph - UpdateLastBlockAndContainer(); - - if (IsLazy && CurrentBlock is { } currentBlock && currentBlock.IsParagraphBlock) - { - Debug.Assert(NewBlocks.Count == 0); + // Special case for paragraph + UpdateLastBlockAndContainer(); - if (!result.IsDiscard()) - { - if (TrackTrivia) - { - UnwindAllIndents(); - } + if (IsLazy && CurrentBlock is { } currentBlock && currentBlock.IsParagraphBlock) + { + Debug.Assert(NewBlocks.Count == 0); - Unsafe.As(currentBlock).AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia); - } + if (!result.IsDiscard()) + { if (TrackTrivia) { - // special case: take care when refactoring this - if (currentBlock.Parent is QuoteBlock qb) - { - var triviaAfter = UseTrivia(Start - 1); - qb.QuoteLines.Last().TriviaAfter = triviaAfter; - } + UnwindAllIndents(); } - // We have just found a lazy continuation for a paragraph, early exit - // Mark all block opened after a lazy continuation - OpenAll(); - ContinueProcessingLine = false; - break; + Unsafe.As(currentBlock).AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia); } - - // Nothing found but the BlockParser may instruct to break, so early exit - if (NewBlocks.Count == 0 && result == BlockState.BreakDiscard) + if (TrackTrivia) { - ContinueProcessingLine = false; - break; + // special case: take care when refactoring this + if (currentBlock.Parent is QuoteBlock qb) + { + var triviaAfter = UseTrivia(Start - 1); + qb.QuoteLines.Last().TriviaAfter = triviaAfter; + } } + // We have just found a lazy continuation for a paragraph, early exit + // Mark all block opened after a lazy continuation + OpenAll(); - // If we have a container, we can retry to match against all types of block. - ProcessNewBlocks(result, true); - return ContinueProcessingLine; + ContinueProcessingLine = false; + break; + } - // We have a leaf node, we can stop + // Nothing found but the BlockParser may instruct to break, so early exit + if (NewBlocks.Count == 0 && result == BlockState.BreakDiscard) + { + ContinueProcessingLine = false; + break; } - IsLazy = false; - return false; + // If we have a container, we can retry to match against all types of block. + ProcessNewBlocks(result, true); + return ContinueProcessingLine; + + // We have a leaf node, we can stop } - /// - /// Processes any new blocks that have been pushed to . - /// - /// The last result of matching. - /// if set to true the processing of a new block will close existing opened blocks]. - /// The NewBlocks is not empty. This is happening if a LeafBlock is not the last to be pushed - private void ProcessNewBlocks(BlockState result, bool allowClosing) + IsLazy = false; + return false; + } + + /// + /// Processes any new blocks that have been pushed to . + /// + /// The last result of matching. + /// if set to true the processing of a new block will close existing opened blocks]. + /// The NewBlocks is not empty. This is happening if a LeafBlock is not the last to be pushed + private void ProcessNewBlocks(BlockState result, bool allowClosing) + { + var newBlocks = NewBlocks; + while (newBlocks.Count > 0) { - var newBlocks = NewBlocks; - while (newBlocks.Count > 0) - { - var block = newBlocks.Pop(); + var block = newBlocks.Pop(); - if (block.Parser is null) - { - ThrowHelper.InvalidOperationException($"The new block [{block.GetType()}] must have a valid Parser property"); - } + if (block.Parser is null) + { + ThrowHelper.InvalidOperationException($"The new block [{block.GetType()}] must have a valid Parser property"); + } - block.Line = LineIndex; + block.Line = LineIndex; - // If we have a leaf block - if (block.IsLeafBlock) + // If we have a leaf block + if (block.IsLeafBlock) + { + if (!result.IsDiscard()) { - if (!result.IsDiscard()) + if (TrackTrivia) { - if (TrackTrivia) + if (block.IsParagraphBlock || block is HtmlBlock) { - if (block.IsParagraphBlock || block is HtmlBlock) - { - UnwindAllIndents(); - } + UnwindAllIndents(); } - - Unsafe.As(block).AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia); - } - - if (newBlocks.Count > 0) - { - ThrowHelper.InvalidOperationException( - "The NewBlocks is not empty. This is happening if a LeafBlock is not the last to be pushed"); } - } - if (allowClosing) - { - // Close any previous blocks not opened - CloseAll(false); + Unsafe.As(block).AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia); } - // If previous block is a container, add the new block as a children of the previous block - if (block.Parent is null) + if (newBlocks.Count > 0) { - UpdateLastBlockAndContainer(); - CurrentContainer!.Add(block); + ThrowHelper.InvalidOperationException( + "The NewBlocks is not empty. This is happening if a LeafBlock is not the last to be pushed"); } + } - block.IsOpen = result.IsContinue(); - - // Add a block BlockProcessor to the stack (and leave it opened) - OpenedBlocks.Add(block); + if (allowClosing) + { + // Close any previous blocks not opened + CloseAll(false); + } - if (block.IsLeafBlock) - { - ContinueProcessingLine = false; - return; - } + // If previous block is a container, add the new block as a children of the previous block + if (block.Parent is null) + { + UpdateLastBlockAndContainer(); + CurrentContainer!.Add(block); } - ContinueProcessingLine = !result.IsDiscard(); - } + block.IsOpen = result.IsContinue(); - private void ResetLine(StringSlice newLine) - { - Line = newLine; - Column = 0; - ColumnBeforeIndent = 0; - StartBeforeIndent = Start; - originalLineStart = newLine.Start; - TriviaStart = newLine.Start; - } + // Add a block BlockProcessor to the stack (and leave it opened) + OpenedBlocks.Add(block); + if (block.IsLeafBlock) + { + ContinueProcessingLine = false; + return; + } + } + ContinueProcessingLine = !result.IsDiscard(); + } - [MemberNotNull(nameof(Document), nameof(Parsers))] - internal void Setup(MarkdownDocument document, BlockParserList parsers, MarkdownParserContext? context, bool trackTrivia) - { - if (document is null) ThrowHelper.ArgumentNullException(nameof(document)); - if (parsers is null) ThrowHelper.ArgumentNullException(nameof(parsers)); + private void ResetLine(StringSlice newLine) + { + Line = newLine; + Column = 0; + ColumnBeforeIndent = 0; + StartBeforeIndent = Start; + originalLineStart = newLine.Start; + TriviaStart = newLine.Start; + } - Document = document; - Parsers = parsers; - Context = context; - TrackTrivia = trackTrivia; - } - private void Reset() - { - Document = null!; - Parsers = null!; - Context = null; - CurrentContainer = null; - CurrentBlock = null; - LastBlock = null; - - TrackTrivia = false; - SkipFirstUnwindSpace = false; - ContinueProcessingLine = false; - IsLazy = false; - currentStackIndex = 0; - originalLineStart = 0; - CurrentLineStartPosition = 0; - ColumnBeforeIndent = 0; - StartBeforeIndent = 0; - LineIndex = 0; - Column = 0; - TriviaStart = 0; + [MemberNotNull(nameof(Document), nameof(Parsers))] + internal void Setup(MarkdownDocument document, BlockParserList parsers, MarkdownParserContext? context, bool trackTrivia) + { + if (document is null) ThrowHelper.ArgumentNullException(nameof(document)); + if (parsers is null) ThrowHelper.ArgumentNullException(nameof(parsers)); - Line = StringSlice.Empty; + Document = document; + Parsers = parsers; + Context = context; + TrackTrivia = trackTrivia; + } - NewBlocks.Clear(); - OpenedBlocks.Clear(); - LinesBefore = null; - } + private void Reset() + { + Document = null!; + Parsers = null!; + Context = null; + CurrentContainer = null; + CurrentBlock = null; + LastBlock = null; + + TrackTrivia = false; + SkipFirstUnwindSpace = false; + ContinueProcessingLine = false; + IsLazy = false; + + currentStackIndex = 0; + originalLineStart = 0; + CurrentLineStartPosition = 0; + ColumnBeforeIndent = 0; + StartBeforeIndent = 0; + LineIndex = 0; + Column = 0; + TriviaStart = 0; + + Line = StringSlice.Empty; + + NewBlocks.Clear(); + OpenedBlocks.Clear(); + LinesBefore = null; + } - public BlockProcessor CreateChild() => Rent(Document, Parsers, Context, TrackTrivia); + public BlockProcessor CreateChild() => Rent(Document, Parsers, Context, TrackTrivia); - public void ReleaseChild() => Release(this); + public void ReleaseChild() => Release(this); - private static readonly BlockProcessorCache _cache = new(); + private static readonly BlockProcessorCache _cache = new(); - internal static BlockProcessor Rent(MarkdownDocument document, BlockParserList parsers, MarkdownParserContext? context, bool trackTrivia) - { - var processor = _cache.Get(); - processor.Setup(document, parsers, context, trackTrivia); - return processor; - } + internal static BlockProcessor Rent(MarkdownDocument document, BlockParserList parsers, MarkdownParserContext? context, bool trackTrivia) + { + var processor = _cache.Get(); + processor.Setup(document, parsers, context, trackTrivia); + return processor; + } - internal static void Release(BlockProcessor processor) - { - _cache.Release(processor); - } + internal static void Release(BlockProcessor processor) + { + _cache.Release(processor); + } - private sealed class BlockProcessorCache : ObjectCache - { - protected override BlockProcessor NewInstance() => new BlockProcessor(); + private sealed class BlockProcessorCache : ObjectCache + { + protected override BlockProcessor NewInstance() => new BlockProcessor(); - protected override void Reset(BlockProcessor instance) => instance.Reset(); - } + protected override void Reset(BlockProcessor instance) => instance.Reset(); } } \ No newline at end of file diff --git a/src/Markdig/Parsers/BlockState.cs b/src/Markdig/Parsers/BlockState.cs index 5ad5e3fdc..bd1fbc7f4 100644 --- a/src/Markdig/Parsers/BlockState.cs +++ b/src/Markdig/Parsers/BlockState.cs @@ -1,42 +1,41 @@ -// Copyright (c) Alexandre Mutel. All rights reserved. +// 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 Markdig.Parsers +namespace Markdig.Parsers; + +/// +/// Defines the result of parsing a line for a . +/// +public enum BlockState { /// - /// Defines the result of parsing a line for a . + /// A line is not accepted by this parser. /// - public enum BlockState - { - /// - /// A line is not accepted by this parser. - /// - None, + None, - /// - /// The parser is skipped. - /// - Skip, + /// + /// The parser is skipped. + /// + Skip, - /// - /// The parser accepts a line and instruct to continue. - /// - Continue, + /// + /// The parser accepts a line and instruct to continue. + /// + Continue, - /// - /// The parser accepts a line, instruct to continue but discard the line (not stored on the block) - /// - ContinueDiscard, + /// + /// The parser accepts a line, instruct to continue but discard the line (not stored on the block) + /// + ContinueDiscard, - /// - /// The parser is ending a block, instruct to stop and keep the line being processed. - /// - Break, + /// + /// The parser is ending a block, instruct to stop and keep the line being processed. + /// + Break, - /// - /// The parser is ending a block, instruct to stop and discard the line being processed. - /// - BreakDiscard - } + /// + /// The parser is ending a block, instruct to stop and discard the line being processed. + /// + BreakDiscard } \ No newline at end of file diff --git a/src/Markdig/Parsers/BlockStateExtensions.cs b/src/Markdig/Parsers/BlockStateExtensions.cs index 081e5efff..1fae416cf 100644 --- a/src/Markdig/Parsers/BlockStateExtensions.cs +++ b/src/Markdig/Parsers/BlockStateExtensions.cs @@ -4,44 +4,43 @@ using System.Runtime.CompilerServices; -namespace Markdig.Parsers +namespace Markdig.Parsers; + +/// +/// Extensions used by . +/// +public static class BlockStateExtensions { /// - /// Extensions used by . + /// Determines whether this is discarded. /// - public static class BlockStateExtensions + /// State of the block. + /// true if the block state is in discard state + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsDiscard(this BlockState blockState) { - /// - /// Determines whether this is discarded. - /// - /// State of the block. - /// true if the block state is in discard state - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsDiscard(this BlockState blockState) - { - return blockState == BlockState.ContinueDiscard || blockState == BlockState.BreakDiscard; - } + return blockState == BlockState.ContinueDiscard || blockState == BlockState.BreakDiscard; + } - /// - /// Determines whether this is in a continue state. - /// - /// State of the block. - /// true if the block state is in continue state - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsContinue(this BlockState blockState) - { - return blockState == BlockState.Continue || blockState == BlockState.ContinueDiscard; - } + /// + /// Determines whether this is in a continue state. + /// + /// State of the block. + /// true if the block state is in continue state + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsContinue(this BlockState blockState) + { + return blockState == BlockState.Continue || blockState == BlockState.ContinueDiscard; + } - /// - /// Determines whether this is in a break state. - /// - /// State of the block. - /// true if the block state is in break state - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsBreak(this BlockState blockState) - { - return blockState == BlockState.Break || blockState == BlockState.BreakDiscard; - } + /// + /// Determines whether this is in a break state. + /// + /// State of the block. + /// true if the block state is in break state + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsBreak(this BlockState blockState) + { + return blockState == BlockState.Break || blockState == BlockState.BreakDiscard; } } \ No newline at end of file diff --git a/src/Markdig/Parsers/FencedBlockParserBase.cs b/src/Markdig/Parsers/FencedBlockParserBase.cs index b48e41bd0..1ffab620f 100644 --- a/src/Markdig/Parsers/FencedBlockParserBase.cs +++ b/src/Markdig/Parsers/FencedBlockParserBase.cs @@ -2,371 +2,370 @@ // 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 Markdig.Helpers; using Markdig.Renderers.Html; using Markdig.Syntax; -namespace Markdig.Parsers +namespace Markdig.Parsers; + +public abstract class FencedBlockParserBase : BlockParser, IAttributesParseable +{ + /// + /// Delegate used to parse the string on the first line after the fenced code block special characters (usually ` or ~) + /// + /// The parser processor. + /// The being processed line. + /// The fenced code block. + /// The opening character for the fenced code block (usually ` or ~) + /// true if parsing of the line is successfull; false otherwise + public delegate bool InfoParserDelegate(BlockProcessor state, ref StringSlice line, IFencedBlock fenced, char openingCharacter); + + + /// + /// Gets or sets the information parser. + /// + public InfoParserDelegate? InfoParser { get; set; } + + /// + /// A delegates that allows to process attached attributes + /// + public TryParseAttributesDelegate? TryParseAttributes { get; set; } +} + +/// +/// Base parser for fenced blocks (opened by 3 or more character delimiters on a first line, and closed by at least the same number of delimiters) +/// +/// +public abstract class FencedBlockParserBase : FencedBlockParserBase where T : Block, IFencedBlock { - public abstract class FencedBlockParserBase : BlockParser, IAttributesParseable + private static readonly TransformedStringCache _infoStringCache = new(static infoString => HtmlHelper.Unescape(infoString)); + private TransformedStringCache? _infoPrefixCache; + + /// + /// Initializes a new instance of the class. + /// + protected FencedBlockParserBase() { - /// - /// Delegate used to parse the string on the first line after the fenced code block special characters (usually ` or ~) - /// - /// The parser processor. - /// The being processed line. - /// The fenced code block. - /// The opening character for the fenced code block (usually ` or ~) - /// true if parsing of the line is successfull; false otherwise - public delegate bool InfoParserDelegate(BlockProcessor state, ref StringSlice line, IFencedBlock fenced, char openingCharacter); - - - /// - /// Gets or sets the information parser. - /// - public InfoParserDelegate? InfoParser { get; set; } - - /// - /// A delegates that allows to process attached attributes - /// - public TryParseAttributesDelegate? TryParseAttributes { get; set; } + InfoParser = DefaultInfoParser; + MinimumMatchCount = 3; + MaximumMatchCount = int.MaxValue; } + private string? _infoPrefix; /// - /// Base parser for fenced blocks (opened by 3 or more character delimiters on a first line, and closed by at least the same number of delimiters) + /// Gets or sets the language prefix (default is "language-") /// - /// - public abstract class FencedBlockParserBase : FencedBlockParserBase where T : Block, IFencedBlock + public string? InfoPrefix { - private static readonly TransformedStringCache _infoStringCache = new(static infoString => HtmlHelper.Unescape(infoString)); - private TransformedStringCache? _infoPrefixCache; - - /// - /// Initializes a new instance of the class. - /// - protected FencedBlockParserBase() + get => _infoPrefix; + set { - InfoParser = DefaultInfoParser; - MinimumMatchCount = 3; - MaximumMatchCount = int.MaxValue; - } - - private string? _infoPrefix; - /// - /// Gets or sets the language prefix (default is "language-") - /// - public string? InfoPrefix - { - get => _infoPrefix; - set + if (_infoPrefix != value) { - if (_infoPrefix != value) - { - _infoPrefixCache = new TransformedStringCache(infoString => value + infoString); - _infoPrefix = value; - } + _infoPrefixCache = new TransformedStringCache(infoString => value + infoString); + _infoPrefix = value; } } + } - public int MinimumMatchCount { get; set; } + public int MinimumMatchCount { get; set; } - public int MaximumMatchCount { get; set; } + public int MaximumMatchCount { get; set; } - private enum ParseState - { - AfterFence, - Info, - AfterInfo, - Args, - AfterArgs, - } + private enum ParseState + { + AfterFence, + Info, + AfterInfo, + Args, + AfterArgs, + } - /// - /// The roundtrip parser for the information after the fenced code block special characters (usually ` or ~) - /// - /// The parser processor. - /// The line. - /// The fenced code block. - /// The opening character for this fenced code block. - /// true if parsing of the line is successfull; false otherwise - public static bool RoundtripInfoParser(BlockProcessor blockProcessor, ref StringSlice line, IFencedBlock fenced, char openingCharacter) + /// + /// The roundtrip parser for the information after the fenced code block special characters (usually ` or ~) + /// + /// The parser processor. + /// The line. + /// The fenced code block. + /// The opening character for this fenced code block. + /// true if parsing of the line is successfull; false otherwise + public static bool RoundtripInfoParser(BlockProcessor blockProcessor, ref StringSlice line, IFencedBlock fenced, char openingCharacter) + { + var start = line.Start; + var end = start - 1; + var afterFence = new StringSlice(line.Text, start, end); + var info = new StringSlice(line.Text, start, end); + var afterInfo = new StringSlice(line.Text, start, end); + var arg = new StringSlice(line.Text, start, end); + var afterArg = new StringSlice(line.Text, start, end); + ParseState state = ParseState.AfterFence; + + for (int i = line.Start; i <= line.End; i++) { - var start = line.Start; - var end = start - 1; - var afterFence = new StringSlice(line.Text, start, end); - var info = new StringSlice(line.Text, start, end); - var afterInfo = new StringSlice(line.Text, start, end); - var arg = new StringSlice(line.Text, start, end); - var afterArg = new StringSlice(line.Text, start, end); - ParseState state = ParseState.AfterFence; - - for (int i = line.Start; i <= line.End; i++) + char c = line.Text[i]; + // An info string cannot contain any backticks (unless it is a tilde block) + if (c == '`' && openingCharacter == '`') { - char c = line.Text[i]; - // An info string cannot contain any backticks (unless it is a tilde block) - if (c == '`' && openingCharacter == '`') - { - return false; - } - switch (state) - { - case ParseState.AfterFence: - if (c.IsSpaceOrTab()) - { - afterFence.End += 1; - } - else - { - state = ParseState.Info; - info.Start = i; - info.End = i; - afterFence.End = i - 1; - } - break; - case ParseState.Info: - if (c.IsSpaceOrTab()) - { - state = ParseState.AfterInfo; - afterInfo.Start = i; - afterInfo.End = i; - } - else - { - info.End += 1; - } - break; - case ParseState.AfterInfo: + return false; + } + switch (state) + { + case ParseState.AfterFence: + if (c.IsSpaceOrTab()) + { + afterFence.End += 1; + } + else + { + state = ParseState.Info; + info.Start = i; + info.End = i; + afterFence.End = i - 1; + } + break; + case ParseState.Info: + if (c.IsSpaceOrTab()) + { + state = ParseState.AfterInfo; + afterInfo.Start = i; + afterInfo.End = i; + } + else + { + info.End += 1; + } + break; + case ParseState.AfterInfo: + if (c.IsSpaceOrTab()) + { + afterInfo.End += 1; + } + else + { + arg.Start = i; + arg.End = i; + state = ParseState.Args; + } + break; + case ParseState.Args: + // walk from end, as rest (except trailing spaces) is args + for (int j = line.End; j > start; j--) + { + c = line[j]; if (c.IsSpaceOrTab()) { - afterInfo.End += 1; + afterArg.Start = i; } else { - arg.Start = i; - arg.End = i; - state = ParseState.Args; - } - break; - case ParseState.Args: - // walk from end, as rest (except trailing spaces) is args - for (int j = line.End; j > start; j--) - { - c = line[j]; - if (c.IsSpaceOrTab()) - { - afterArg.Start = i; - } - else - { - arg.End = j; - afterArg.Start = j + 1; - afterArg.End = line.End; - goto end; - } - } - goto end; - case ParseState.AfterArgs: - { - return false; + arg.End = j; + afterArg.Start = j + 1; + afterArg.End = line.End; + goto end; } - } + } + goto end; + case ParseState.AfterArgs: + { + return false; + } } - - end: - fenced.TriviaAfterFencedChar = afterFence; - fenced.Info = _infoStringCache.Get(info.AsSpan()); - fenced.UnescapedInfo = info; - fenced.TriviaAfterInfo = afterInfo; - fenced.Arguments = HtmlHelper.Unescape(arg.ToString()); - fenced.UnescapedArguments = arg; - fenced.TriviaAfterArguments = afterArg; - fenced.InfoNewLine = line.NewLine; - - return true; } - /// - /// The default parser for the information after the fenced code block special characters (usually ` or ~) - /// - /// The parser processor. - /// The line. - /// The fenced code block. - /// The opening character for this fenced code block. - /// true if parsing of the line is successfull; false otherwise - public static bool DefaultInfoParser(BlockProcessor state, ref StringSlice line, IFencedBlock fenced, char openingCharacter) + end: + fenced.TriviaAfterFencedChar = afterFence; + fenced.Info = _infoStringCache.Get(info.AsSpan()); + fenced.UnescapedInfo = info; + fenced.TriviaAfterInfo = afterInfo; + fenced.Arguments = HtmlHelper.Unescape(arg.ToString()); + fenced.UnescapedArguments = arg; + fenced.TriviaAfterArguments = afterArg; + fenced.InfoNewLine = line.NewLine; + + return true; + } + + /// + /// The default parser for the information after the fenced code block special characters (usually ` or ~) + /// + /// The parser processor. + /// The line. + /// The fenced code block. + /// The opening character for this fenced code block. + /// true if parsing of the line is successfull; false otherwise + public static bool DefaultInfoParser(BlockProcessor state, ref StringSlice line, IFencedBlock fenced, char openingCharacter) + { + // An info string cannot contain any backticks (unless it is a tilde block) + int firstSpace = -1; + if (openingCharacter == '`') { - // An info string cannot contain any backticks (unless it is a tilde block) - int firstSpace = -1; - if (openingCharacter == '`') + for (int i = line.Start; i <= line.End; i++) { - for (int i = line.Start; i <= line.End; i++) + char c = line.Text[i]; + if (c == '`') { - char c = line.Text[i]; - if (c == '`') - { - return false; - } + return false; + } - if (firstSpace < 0 && c.IsSpaceOrTab()) - { - firstSpace = i; - } + if (firstSpace < 0 && c.IsSpaceOrTab()) + { + firstSpace = i; } } - else + } + else + { + for (int i = line.Start; i <= line.End; i++) { - for (int i = line.Start; i <= line.End; i++) + if (line.Text[i].IsSpaceOrTab()) { - if (line.Text[i].IsSpaceOrTab()) - { - firstSpace = i; - break; - } + firstSpace = i; + break; } } + } - StringSlice infoStringSlice; - string? argString = null; + StringSlice infoStringSlice; + string? argString = null; - if (firstSpace > 0) - { - infoStringSlice = new StringSlice(line.Text, line.Start, firstSpace - 1); + if (firstSpace > 0) + { + infoStringSlice = new StringSlice(line.Text, line.Start, firstSpace - 1); - // Skip any spaces after info string - firstSpace++; - while (firstSpace <= line.End) + // Skip any spaces after info string + firstSpace++; + while (firstSpace <= line.End) + { + char c = line[firstSpace]; + if (c.IsSpaceOrTab()) { - char c = line[firstSpace]; - if (c.IsSpaceOrTab()) - { - firstSpace++; - } - else - { - break; - } + firstSpace++; + } + else + { + break; } - - var argStringSlice = new StringSlice(line.Text, firstSpace, line.End); - argStringSlice.Trim(); - argString = argStringSlice.ToString(); - } - else - { - infoStringSlice = line; } - infoStringSlice.Trim(); - - fenced.Info = _infoStringCache.Get(infoStringSlice.AsSpan()); - fenced.Arguments = HtmlHelper.Unescape(argString); - - return true; + var argStringSlice = new StringSlice(line.Text, firstSpace, line.End); + argStringSlice.Trim(); + argString = argStringSlice.ToString(); } - - public override BlockState TryOpen(BlockProcessor processor) + else { - // We expect no indentation for a fenced code block. - if (processor.IsCodeIndent) - { - return BlockState.None; - } + infoStringSlice = line; + } - // Match fenced char - var line = processor.Line; - char matchChar = line.CurrentChar; - int count = line.CountAndSkipChar(matchChar); + infoStringSlice.Trim(); - // A fenced codeblock requires at least 3 opening chars - if (count < MinimumMatchCount || count > MaximumMatchCount) - { - return BlockState.None; - } + fenced.Info = _infoStringCache.Get(infoStringSlice.AsSpan()); + fenced.Arguments = HtmlHelper.Unescape(argString); - // specs spaces: Is space and tabs? or only spaces? Use space and tab for this case - if (!processor.TrackTrivia) - { - line.TrimStart(); - } + return true; + } - var fenced = CreateFencedBlock(processor); - { - fenced.Column = processor.Column; - fenced.FencedChar = matchChar; - fenced.OpeningFencedCharCount = count; - fenced.Span.Start = processor.Start; - fenced.Span.End = line.Start; - }; - - // Try to parse any attached attributes - TryParseAttributes?.Invoke(processor, ref line, fenced); - - // If the info parser was not successfull, early exit - if (InfoParser != null && !InfoParser(processor, ref line, fenced, matchChar)) - { - return BlockState.None; - } + public override BlockState TryOpen(BlockProcessor processor) + { + // We expect no indentation for a fenced code block. + if (processor.IsCodeIndent) + { + return BlockState.None; + } - // Add the language as an attribute by default - if (!string.IsNullOrEmpty(fenced.Info)) - { - Debug.Assert(_infoPrefixCache is not null || InfoPrefix is null); - string infoWithPrefix = _infoPrefixCache?.Get(fenced.Info!) ?? fenced.Info!; - fenced.GetAttributes().AddClass(infoWithPrefix); - } + // Match fenced char + var line = processor.Line; + char matchChar = line.CurrentChar; + int count = line.CountAndSkipChar(matchChar); - // Store the number of matched string into the context - processor.NewBlocks.Push(fenced); + // A fenced codeblock requires at least 3 opening chars + if (count < MinimumMatchCount || count > MaximumMatchCount) + { + return BlockState.None; + } - // Discard the current line as it is already parsed - return BlockState.ContinueDiscard; + // specs spaces: Is space and tabs? or only spaces? Use space and tab for this case + if (!processor.TrackTrivia) + { + line.TrimStart(); } - protected abstract T CreateFencedBlock(BlockProcessor processor); + var fenced = CreateFencedBlock(processor); + { + fenced.Column = processor.Column; + fenced.FencedChar = matchChar; + fenced.OpeningFencedCharCount = count; + fenced.Span.Start = processor.Start; + fenced.Span.End = line.Start; + }; + + // Try to parse any attached attributes + TryParseAttributes?.Invoke(processor, ref line, fenced); + + // If the info parser was not successfull, early exit + if (InfoParser != null && !InfoParser(processor, ref line, fenced, matchChar)) + { + return BlockState.None; + } - public override BlockState TryContinue(BlockProcessor processor, Block block) + // Add the language as an attribute by default + if (!string.IsNullOrEmpty(fenced.Info)) { - var fence = (IFencedBlock)block; - var openingCount = fence.OpeningFencedCharCount; - - // Match if we have a closing fence - var line = processor.Line; - var sourcePosition = processor.Start; - var closingCount = line.CountAndSkipChar(fence.FencedChar); - var diff = openingCount - closingCount; - - char c = line.CurrentChar; - var lastFenceCharPosition = processor.Start + closingCount; - - // If we have a closing fence, close it and discard the current line - // The line must contain only fence opening character followed only by whitespaces. - var startBeforeTrim = line.Start; - var endBeforeTrim = line.End; - var trimmed = line.TrimEnd(); - if (diff <= 0 && !processor.IsCodeIndent && (c == '\0' || c.IsWhitespace()) && trimmed) - { - block.UpdateSpanEnd(startBeforeTrim - 1); + Debug.Assert(_infoPrefixCache is not null || InfoPrefix is null); + string infoWithPrefix = _infoPrefixCache?.Get(fenced.Info!) ?? fenced.Info!; + fenced.GetAttributes().AddClass(infoWithPrefix); + } - var fencedBlock = (IFencedBlock)block; - fencedBlock.ClosingFencedCharCount = closingCount; + // Store the number of matched string into the context + processor.NewBlocks.Push(fenced); - if (processor.TrackTrivia) - { - fencedBlock.NewLine = processor.Line.NewLine; - fencedBlock.TriviaBeforeClosingFence = processor.UseTrivia(sourcePosition - 1); - fencedBlock.TriviaAfter = new StringSlice(processor.Line.Text, lastFenceCharPosition, endBeforeTrim); - } + // Discard the current line as it is already parsed + return BlockState.ContinueDiscard; + } - // Don't keep the last line - return BlockState.BreakDiscard; - } + protected abstract T CreateFencedBlock(BlockProcessor processor); - // Reset the indentation to the column before the indent - processor.GoToColumn(processor.ColumnBeforeIndent); + public override BlockState TryContinue(BlockProcessor processor, Block block) + { + var fence = (IFencedBlock)block; + var openingCount = fence.OpeningFencedCharCount; + + // Match if we have a closing fence + var line = processor.Line; + var sourcePosition = processor.Start; + var closingCount = line.CountAndSkipChar(fence.FencedChar); + var diff = openingCount - closingCount; + + char c = line.CurrentChar; + var lastFenceCharPosition = processor.Start + closingCount; + + // If we have a closing fence, close it and discard the current line + // The line must contain only fence opening character followed only by whitespaces. + var startBeforeTrim = line.Start; + var endBeforeTrim = line.End; + var trimmed = line.TrimEnd(); + if (diff <= 0 && !processor.IsCodeIndent && (c == '\0' || c.IsWhitespace()) && trimmed) + { + block.UpdateSpanEnd(startBeforeTrim - 1); - return BlockState.Continue; + var fencedBlock = (IFencedBlock)block; + fencedBlock.ClosingFencedCharCount = closingCount; + + 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; } + + // Reset the indentation to the column before the indent + processor.GoToColumn(processor.ColumnBeforeIndent); + + return BlockState.Continue; } } \ No newline at end of file diff --git a/src/Markdig/Parsers/FencedCodeBlockParser.cs b/src/Markdig/Parsers/FencedCodeBlockParser.cs index fdfa7d06f..706f52a76 100644 --- a/src/Markdig/Parsers/FencedCodeBlockParser.cs +++ b/src/Markdig/Parsers/FencedCodeBlockParser.cs @@ -5,59 +5,58 @@ using Markdig.Helpers; using Markdig.Syntax; -namespace Markdig.Parsers +namespace Markdig.Parsers; + +/// +/// Parser for a . +/// +/// +public class FencedCodeBlockParser : FencedBlockParserBase { + public const string DefaultInfoPrefix = "language-"; + /// - /// Parser for a . + /// Initializes a new instance of the class. /// - /// - public class FencedCodeBlockParser : FencedBlockParserBase + public FencedCodeBlockParser() { - public const string DefaultInfoPrefix = "language-"; + OpeningCharacters = new[] {'`', '~'}; + InfoPrefix = DefaultInfoPrefix; + } - /// - /// Initializes a new instance of the class. - /// - public FencedCodeBlockParser() + protected override FencedCodeBlock CreateFencedBlock(BlockProcessor processor) + { + var codeBlock = new FencedCodeBlock(this) { - OpeningCharacters = new[] {'`', '~'}; - InfoPrefix = DefaultInfoPrefix; - } + IndentCount = processor.Indent, + }; - protected override FencedCodeBlock CreateFencedBlock(BlockProcessor processor) + if (processor.TrackTrivia) { - var codeBlock = new FencedCodeBlock(this) - { - IndentCount = processor.Indent, - }; - - if (processor.TrackTrivia) - { - codeBlock.LinesBefore = processor.UseLinesBefore(); - codeBlock.TriviaBefore = processor.UseTrivia(processor.Start - 1); - codeBlock.NewLine = processor.Line.NewLine; - } - - return codeBlock; + codeBlock.LinesBefore = processor.UseLinesBefore(); + codeBlock.TriviaBefore = processor.UseTrivia(processor.Start - 1); + codeBlock.NewLine = processor.Line.NewLine; } - public override BlockState TryContinue(BlockProcessor processor, Block block) + return codeBlock; + } + + public override BlockState TryContinue(BlockProcessor processor, Block block) + { + var result = base.TryContinue(processor, block); + if (result == BlockState.Continue && !processor.TrackTrivia) { - var result = base.TryContinue(processor, block); - if (result == BlockState.Continue && !processor.TrackTrivia) + var fence = (FencedCodeBlock)block; + // Remove any indent spaces + var c = processor.CurrentChar; + var indentCount = fence.IndentCount; + while (indentCount > 0 && c.IsSpace()) { - var fence = (FencedCodeBlock)block; - // Remove any indent spaces - var c = processor.CurrentChar; - var indentCount = fence.IndentCount; - while (indentCount > 0 && c.IsSpace()) - { - indentCount--; - c = processor.NextChar(); - } + indentCount--; + c = processor.NextChar(); } - - return result; } + + return result; } } \ No newline at end of file diff --git a/src/Markdig/Parsers/HeadingBlockParser.cs b/src/Markdig/Parsers/HeadingBlockParser.cs index 8b619d7a6..bf00e685e 100644 --- a/src/Markdig/Parsers/HeadingBlockParser.cs +++ b/src/Markdig/Parsers/HeadingBlockParser.cs @@ -6,174 +6,173 @@ using Markdig.Helpers; using Markdig.Syntax; -namespace Markdig.Parsers +namespace Markdig.Parsers; + +/// +/// Block parser for a . +/// +/// +public class HeadingBlockParser : BlockParser, IAttributesParseable { + /// - /// Block parser for a . + /// Initializes a new instance of the class. /// - /// - public class HeadingBlockParser : BlockParser, IAttributesParseable + public HeadingBlockParser() { + OpeningCharacters = new[] {'#'}; + } - /// - /// Initializes a new instance of the class. - /// - public HeadingBlockParser() - { - OpeningCharacters = new[] {'#'}; - } + /// + /// Gets or sets the max count of the leading unescaped # characters + /// + public int MaxLeadingCount { get; set; } = 6; - /// - /// Gets or sets the max count of the leading unescaped # characters - /// - public int MaxLeadingCount { get; set; } = 6; + /// + /// A delegates that allows to process attached attributes after # + /// + public TryParseAttributesDelegate? TryParseAttributes { get; set; } - /// - /// A delegates that allows to process attached attributes after # - /// - public TryParseAttributesDelegate? TryParseAttributes { get; set; } + public override BlockState TryOpen(BlockProcessor processor) + { + // If we are in a CodeIndent, early exit + if (processor.IsCodeIndent) + { + return BlockState.None; + } - public override BlockState TryOpen(BlockProcessor processor) + // 4.2 ATX headings + // An ATX heading consists of a string of characters, parsed as inline content, + // between an opening sequence of 1–6(configurable) unescaped # characters and an optional + // closing sequence of any number of unescaped # characters. The opening sequence + // of # characters must be followed by a space or by the end of line. The optional + // closing sequence of #s must be preceded by a space and may be followed by spaces + // only. The opening # character may be indented 0-3 spaces. The raw contents of + // the heading are stripped of leading and trailing spaces before being parsed as + // inline content. The heading level is equal to the number of # characters in the + // opening sequence. + var column = processor.Column; + var line = processor.Line; + var sourcePosition = line.Start; + var c = line.CurrentChar; + var matchingChar = c; + + Debug.Assert(MaxLeadingCount > 0); + int leadingCount = 0; + while (c != '\0' && leadingCount <= MaxLeadingCount) { - // If we are in a CodeIndent, early exit - if (processor.IsCodeIndent) + if (c != matchingChar) { - return BlockState.None; + break; } + c = processor.NextChar(); + leadingCount++; + } - // 4.2 ATX headings - // An ATX heading consists of a string of characters, parsed as inline content, - // between an opening sequence of 1–6(configurable) unescaped # characters and an optional - // closing sequence of any number of unescaped # characters. The opening sequence - // of # characters must be followed by a space or by the end of line. The optional - // closing sequence of #s must be preceded by a space and may be followed by spaces - // only. The opening # character may be indented 0-3 spaces. The raw contents of - // the heading are stripped of leading and trailing spaces before being parsed as - // inline content. The heading level is equal to the number of # characters in the - // opening sequence. - var column = processor.Column; - var line = processor.Line; - var sourcePosition = line.Start; - var c = line.CurrentChar; - var matchingChar = c; - - Debug.Assert(MaxLeadingCount > 0); - int leadingCount = 0; - while (c != '\0' && leadingCount <= MaxLeadingCount) + // A space is required after leading # + if (leadingCount > 0 && leadingCount <= MaxLeadingCount && (c.IsSpaceOrTab() || c == '\0')) + { + StringSlice trivia = StringSlice.Empty; + if (processor.TrackTrivia && c.IsSpaceOrTab()) { - if (c != matchingChar) - { - break; - } - c = processor.NextChar(); - leadingCount++; + trivia = new StringSlice(processor.Line.Text, processor.Start, processor.Start); + processor.NextChar(); } + // Move to the content + var headingBlock = new HeadingBlock(this) + { + HeaderChar = matchingChar, + Level = leadingCount, + Column = column, + Span = { Start = sourcePosition }, + }; - // A space is required after leading # - if (leadingCount > 0 && leadingCount <= MaxLeadingCount && (c.IsSpaceOrTab() || c == '\0')) + if (processor.TrackTrivia) { - StringSlice trivia = StringSlice.Empty; - if (processor.TrackTrivia && c.IsSpaceOrTab()) - { - trivia = new StringSlice(processor.Line.Text, processor.Start, processor.Start); - processor.NextChar(); - } - // Move to the content - var headingBlock = new HeadingBlock(this) - { - HeaderChar = matchingChar, - Level = leadingCount, - Column = column, - Span = { Start = sourcePosition }, - }; + headingBlock.TriviaAfterAtxHeaderChar = trivia; + headingBlock.TriviaBefore = processor.UseTrivia(sourcePosition - 1); + headingBlock.LinesBefore = processor.UseLinesBefore(); + headingBlock.NewLine = processor.Line.NewLine; + } + else + { + processor.GoToColumn(column + leadingCount + 1); + } - if (processor.TrackTrivia) + processor.NewBlocks.Push(headingBlock); + + // Gives a chance to parse attributes + TryParseAttributes?.Invoke(processor, ref processor.Line, headingBlock); + + // The optional closing sequence of #s must be preceded by a space and may be followed by spaces only. + int endState = 0; + int countClosingTags = 0; + int sourceEnd = processor.Line.End; + for (int i = processor.Line.End; i >= processor.Line.Start - 1; i--) // Go up to Start - 1 in order to match the space after the first ### + { + c = processor.Line.Text[i]; + if (endState == 0) { - headingBlock.TriviaAfterAtxHeaderChar = trivia; - headingBlock.TriviaBefore = processor.UseTrivia(sourcePosition - 1); - headingBlock.LinesBefore = processor.UseLinesBefore(); - headingBlock.NewLine = processor.Line.NewLine; + if (c.IsSpaceOrTab()) + { + continue; + } + endState = 1; } - else + if (endState == 1) { - processor.GoToColumn(column + leadingCount + 1); - } - - processor.NewBlocks.Push(headingBlock); - - // Gives a chance to parse attributes - TryParseAttributes?.Invoke(processor, ref processor.Line, headingBlock); + if (c == matchingChar) + { + countClosingTags++; + continue; + } - // The optional closing sequence of #s must be preceded by a space and may be followed by spaces only. - int endState = 0; - int countClosingTags = 0; - int sourceEnd = processor.Line.End; - for (int i = processor.Line.End; i >= processor.Line.Start - 1; i--) // Go up to Start - 1 in order to match the space after the first ### - { - c = processor.Line.Text[i]; - if (endState == 0) + if (countClosingTags > 0) { if (c.IsSpaceOrTab()) { - continue; + processor.Line.End = i - 1; } - endState = 1; + break; } - if (endState == 1) + else { - if (c == matchingChar) - { - countClosingTags++; - continue; - } - - if (countClosingTags > 0) - { - if (c.IsSpaceOrTab()) - { - processor.Line.End = i - 1; - } - break; - } - else - { - break; - } + break; } } + } - // Setup the source end position of this element - headingBlock.Span.End = processor.Line.End; + // Setup the source end position of this element + headingBlock.Span.End = processor.Line.End; - if (processor.TrackTrivia) + if (processor.TrackTrivia) + { + var wsa = new StringSlice(processor.Line.Text, processor.Line.End + 1, sourceEnd); + headingBlock.TriviaAfter = wsa; + if (wsa.Overlaps(headingBlock.TriviaAfterAtxHeaderChar)) { - var wsa = new StringSlice(processor.Line.Text, processor.Line.End + 1, sourceEnd); - headingBlock.TriviaAfter = wsa; - if (wsa.Overlaps(headingBlock.TriviaAfterAtxHeaderChar)) - { - // prevent double whitespace allocation in case of closing # i.e. "# #" - headingBlock.TriviaAfterAtxHeaderChar = StringSlice.Empty; - } + // prevent double whitespace allocation in case of closing # i.e. "# #" + headingBlock.TriviaAfterAtxHeaderChar = StringSlice.Empty; } - - // We expect a single line, so don't continue - return BlockState.Break; } - // Else we don't have an header - processor.Line.Start = sourcePosition; - processor.Column = column; - return BlockState.None; + // We expect a single line, so don't continue + return BlockState.Break; } - public override bool Close(BlockProcessor processor, Block block) + // Else we don't have an header + processor.Line.Start = sourcePosition; + processor.Column = column; + return BlockState.None; + } + + public override bool Close(BlockProcessor processor, Block block) + { + if (!processor.TrackTrivia) { - if (!processor.TrackTrivia) - { - var heading = (HeadingBlock)block; - heading.Lines.Trim(); - } - return true; + var heading = (HeadingBlock)block; + heading.Lines.Trim(); } + return true; } } \ No newline at end of file diff --git a/src/Markdig/Parsers/HtmlBlockParser.cs b/src/Markdig/Parsers/HtmlBlockParser.cs index fcaa6f3fc..978441456 100644 --- a/src/Markdig/Parsers/HtmlBlockParser.cs +++ b/src/Markdig/Parsers/HtmlBlockParser.cs @@ -2,370 +2,368 @@ // 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 Markdig.Helpers; using Markdig.Syntax; -namespace Markdig.Parsers +namespace Markdig.Parsers; + +/// +/// Block parser for a . +/// +/// +public class HtmlBlockParser : BlockParser { /// - /// Block parser for a . + /// Initializes a new instance of the class. /// - /// - public class HtmlBlockParser : BlockParser + public HtmlBlockParser() + { + OpeningCharacters = new[] { '<' }; + } + + public override BlockState TryOpen(BlockProcessor processor) { - /// - /// Initializes a new instance of the class. - /// - public HtmlBlockParser() + var result = MatchStart(processor); + + // An end-tag can occur on the same line, so we try to parse it here + if (result == BlockState.Continue) { - OpeningCharacters = new[] { '<' }; + result = MatchEnd(processor, (HtmlBlock) processor.NewBlocks.Peek()); } - - public override BlockState TryOpen(BlockProcessor processor) - { - var result = MatchStart(processor); + return result; + } - // An end-tag can occur on the same line, so we try to parse it here - if (result == BlockState.Continue) - { - result = MatchEnd(processor, (HtmlBlock) processor.NewBlocks.Peek()); - } - return result; - } + public override BlockState TryContinue(BlockProcessor processor, Block block) + { + var htmlBlock = (HtmlBlock) block; + return MatchEnd(processor, htmlBlock); + } - public override BlockState TryContinue(BlockProcessor processor, Block block) + private BlockState MatchStart(BlockProcessor state) + { + if (state.IsCodeIndent) { - var htmlBlock = (HtmlBlock) block; - return MatchEnd(processor, htmlBlock); + return BlockState.None; } - private BlockState MatchStart(BlockProcessor state) - { - if (state.IsCodeIndent) - { - return BlockState.None; - } - - var line = state.Line; - var startPosition = line.Start; - line.SkipChar(); - var result = TryParseTagType16(state, line, state.ColumnBeforeIndent, startPosition); - - // HTML blocks of type 7 cannot interrupt a paragraph: - if (result == BlockState.None && !(state.CurrentBlock is ParagraphBlock)) - { - result = TryParseTagType7(state, line, state.ColumnBeforeIndent, startPosition); - } - return result; - } + var line = state.Line; + var startPosition = line.Start; + line.SkipChar(); + var result = TryParseTagType16(state, line, state.ColumnBeforeIndent, startPosition); - private BlockState TryParseTagType7(BlockProcessor state, StringSlice line, int startColumn, int startPosition) + // HTML blocks of type 7 cannot interrupt a paragraph: + if (result == BlockState.None && !(state.CurrentBlock is ParagraphBlock)) { - var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); - var c = line.CurrentChar; - var result = BlockState.None; - if ((c == '/' && HtmlHelper.TryParseHtmlCloseTag(ref line, ref builder)) || HtmlHelper.TryParseHtmlTagOpenTag(ref line, ref builder)) - { - // Must be followed by whitespace only - bool hasOnlySpaces = true; - c = line.CurrentChar; - while (true) - { - if (c == '\0') - { - break; - } - if (!c.IsWhitespace()) - { - hasOnlySpaces = false; - break; - } - c = line.NextChar(); - } - - if (hasOnlySpaces) - { - result = CreateHtmlBlock(state, HtmlBlockType.NonInterruptingBlock, startColumn, startPosition); - } - } - - builder.Dispose(); - return result; + result = TryParseTagType7(state, line, state.ColumnBeforeIndent, startPosition); } + return result; + } - private BlockState TryParseTagType16(BlockProcessor state, StringSlice line, int startColumn, int startPosition) + private BlockState TryParseTagType7(BlockProcessor state, StringSlice line, int startColumn, int startPosition) + { + var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); + var c = line.CurrentChar; + var result = BlockState.None; + if ((c == '/' && HtmlHelper.TryParseHtmlCloseTag(ref line, ref builder)) || HtmlHelper.TryParseHtmlTagOpenTag(ref line, ref builder)) { - char c; + // Must be followed by whitespace only + bool hasOnlySpaces = true; c = line.CurrentChar; - if (c == '!') + while (true) { - c = line.NextChar(); - if (c == '-' && line.PeekChar() == '-') - { - return CreateHtmlBlock(state, HtmlBlockType.Comment, startColumn, startPosition); // group 2 - } - if (c.IsAlphaUpper()) + if (c == '\0') { - return CreateHtmlBlock(state, HtmlBlockType.DocumentType, startColumn, startPosition); // group 4 + break; } - if (c == '[' && line.Match("CDATA[", 1)) + if (!c.IsWhitespace()) { - return CreateHtmlBlock(state, HtmlBlockType.CData, startColumn, startPosition); // group 5 + hasOnlySpaces = false; + break; } - - return BlockState.None; + c = line.NextChar(); } - if (c == '?') + if (hasOnlySpaces) { - return CreateHtmlBlock(state, HtmlBlockType.ProcessingInstruction, startColumn, startPosition); // group 3 + result = CreateHtmlBlock(state, HtmlBlockType.NonInterruptingBlock, startColumn, startPosition); } + } - var hasLeadingClose = c == '/'; - if (hasLeadingClose) - { - c = line.NextChar(); - } + builder.Dispose(); + return result; + } - Span tag = stackalloc char[10]; - var count = 0; - for (; count < tag.Length; count++) + private BlockState TryParseTagType16(BlockProcessor state, StringSlice line, int startColumn, int startPosition) + { + char c; + c = line.CurrentChar; + if (c == '!') + { + c = line.NextChar(); + if (c == '-' && line.PeekChar() == '-') { - if (!c.IsAlphaNumeric()) - { - break; - } - tag[count] = char.ToLowerInvariant(c); - c = line.NextChar(); + return CreateHtmlBlock(state, HtmlBlockType.Comment, startColumn, startPosition); // group 2 } - - if ( - !(c == '>' || (!hasLeadingClose && c == '/' && line.PeekChar() == '>') || c.IsWhitespace() || - c == '\0')) + if (c.IsAlphaUpper()) { - return BlockState.None; + return CreateHtmlBlock(state, HtmlBlockType.DocumentType, startColumn, startPosition); // group 4 } - - if (count == 0) + if (c == '[' && line.Match("CDATA[", 1)) { - return BlockState.None; + return CreateHtmlBlock(state, HtmlBlockType.CData, startColumn, startPosition); // group 5 } - if (!HtmlTags.TryMatchExact(tag.Slice(0, count), out var match)) - { - return BlockState.None; - } + return BlockState.None; + } - int tagIndex = match.Value; + if (c == '?') + { + return CreateHtmlBlock(state, HtmlBlockType.ProcessingInstruction, startColumn, startPosition); // group 3 + } + + var hasLeadingClose = c == '/'; + if (hasLeadingClose) + { + c = line.NextChar(); + } - // Cannot start with tag = stackalloc char[10]; + var count = 0; + for (; count < tag.Length; count++) + { + if (!c.IsAlphaNumeric()) { - if (c == '/' || hasLeadingClose) - { - return BlockState.None; - } - return CreateHtmlBlock(state, HtmlBlockType.ScriptPreOrStyle, startColumn, startPosition); + break; } + tag[count] = char.ToLowerInvariant(c); + c = line.NextChar(); + } - return CreateHtmlBlock(state, HtmlBlockType.InterruptingBlock, startColumn, startPosition); + if ( + !(c == '>' || (!hasLeadingClose && c == '/' && line.PeekChar() == '>') || c.IsWhitespace() || + c == '\0')) + { + return BlockState.None; } - private const string EndOfComment = "-->"; - private const string EndOfCDATA = "]]>"; - private const string EndOfProcessingInstruction = "?>"; + if (count == 0) + { + return BlockState.None; + } - private BlockState MatchEnd(BlockProcessor state, HtmlBlock htmlBlock) + if (!HtmlTags.TryMatchExact(tag.Slice(0, count), out var match)) { - state.GoToColumn(state.ColumnBeforeIndent); + return BlockState.None; + } + + int tagIndex = match.Value; - // Early exit if it is not starting by an HTML tag - var line = state.Line; - var result = BlockState.Continue; - int index; - switch (htmlBlock.Type) + // Cannot start with = 0) - { - htmlBlock.UpdateSpanEnd(index + EndOfComment.Length); - result = BlockState.Break; - } - break; - case HtmlBlockType.CData: - index = line.IndexOf(EndOfCDATA); - if (index >= 0) - { - htmlBlock.UpdateSpanEnd(index + EndOfCDATA.Length); - result = BlockState.Break; - } - break; - case HtmlBlockType.ProcessingInstruction: - index = line.IndexOf(EndOfProcessingInstruction); - if (index >= 0) - { - htmlBlock.UpdateSpanEnd(index + EndOfProcessingInstruction.Length); - result = BlockState.Break; - } - break; - case HtmlBlockType.DocumentType: - index = line.IndexOf('>'); - if (index >= 0) - { - htmlBlock.UpdateSpanEnd(index + 1); - result = BlockState.Break; - } - break; - case HtmlBlockType.ScriptPreOrStyle: - index = line.IndexOf("", 0, true); + return BlockState.None; + } + return CreateHtmlBlock(state, HtmlBlockType.ScriptPreOrStyle, startColumn, startPosition); + } + + return CreateHtmlBlock(state, HtmlBlockType.InterruptingBlock, startColumn, startPosition); + } + + private const string EndOfComment = "-->"; + private const string EndOfCDATA = "]]>"; + private const string EndOfProcessingInstruction = "?>"; + + private BlockState MatchEnd(BlockProcessor state, HtmlBlock htmlBlock) + { + state.GoToColumn(state.ColumnBeforeIndent); + + // Early exit if it is not starting by an HTML tag + var line = state.Line; + var result = BlockState.Continue; + int index; + switch (htmlBlock.Type) + { + case HtmlBlockType.Comment: + index = line.IndexOf(EndOfComment); + if (index >= 0) + { + htmlBlock.UpdateSpanEnd(index + EndOfComment.Length); + result = BlockState.Break; + } + break; + case HtmlBlockType.CData: + index = line.IndexOf(EndOfCDATA); + if (index >= 0) + { + htmlBlock.UpdateSpanEnd(index + EndOfCDATA.Length); + result = BlockState.Break; + } + break; + case HtmlBlockType.ProcessingInstruction: + index = line.IndexOf(EndOfProcessingInstruction); + if (index >= 0) + { + htmlBlock.UpdateSpanEnd(index + EndOfProcessingInstruction.Length); + result = BlockState.Break; + } + break; + case HtmlBlockType.DocumentType: + index = line.IndexOf('>'); + if (index >= 0) + { + htmlBlock.UpdateSpanEnd(index + 1); + result = BlockState.Break; + } + break; + case HtmlBlockType.ScriptPreOrStyle: + index = line.IndexOf("", 0, true); + if (index >= 0) + { + htmlBlock.UpdateSpanEnd(index + "".Length); + result = BlockState.Break; + } + else + { + index = line.IndexOf("", 0, true); if (index >= 0) { - htmlBlock.UpdateSpanEnd(index + "".Length); + htmlBlock.UpdateSpanEnd(index + "".Length); result = BlockState.Break; } else { - index = line.IndexOf("", 0, true); + index = line.IndexOf("", 0, true); if (index >= 0) { - htmlBlock.UpdateSpanEnd(index + "".Length); + htmlBlock.UpdateSpanEnd(index + "".Length); result = BlockState.Break; } else { - index = line.IndexOf("", 0, true); + index = line.IndexOf("", 0, true); if (index >= 0) { - htmlBlock.UpdateSpanEnd(index + "".Length); + htmlBlock.UpdateSpanEnd(index + "".Length); result = BlockState.Break; } - else - { - index = line.IndexOf("", 0, true); - if (index >= 0) - { - htmlBlock.UpdateSpanEnd(index + "".Length); - result = BlockState.Break; - } - } } } - break; - case HtmlBlockType.InterruptingBlock: - if (state.IsBlankLine) - { - result = BlockState.BreakDiscard; - } - break; - case HtmlBlockType.NonInterruptingBlock: - if (state.IsBlankLine) - { - result = BlockState.BreakDiscard; - } - break; - } - - // Update only if we don't have a break discard - if (result != BlockState.BreakDiscard) - { - htmlBlock.Span.End = line.End; - htmlBlock.NewLine = state.Line.NewLine; - } - - return result; + } + break; + case HtmlBlockType.InterruptingBlock: + if (state.IsBlankLine) + { + result = BlockState.BreakDiscard; + } + break; + case HtmlBlockType.NonInterruptingBlock: + if (state.IsBlankLine) + { + result = BlockState.BreakDiscard; + } + break; } - private BlockState CreateHtmlBlock(BlockProcessor state, HtmlBlockType type, int startColumn, int startPosition) + // Update only if we don't have a break discard + if (result != BlockState.BreakDiscard) { - 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), - }; - - if (state.TrackTrivia) - { - htmlBlock.LinesBefore = state.UseLinesBefore(); - htmlBlock.NewLine = state.Line.NewLine; - } - - state.NewBlocks.Push(htmlBlock); - return BlockState.Continue; + htmlBlock.Span.End = line.End; + htmlBlock.NewLine = state.Line.NewLine; } - private static readonly CompactPrefixTree HtmlTags = new(66, 94, 83) + return result; + } + + private BlockState CreateHtmlBlock(BlockProcessor state, HtmlBlockType type, int startColumn, int startPosition) + { + var htmlBlock = new HtmlBlock(this) { - { "address", 0 }, - { "article", 1 }, - { "aside", 2 }, - { "base", 3 }, - { "basefont", 4 }, - { "blockquote", 5 }, - { "body", 6 }, - { "caption", 7 }, - { "center", 8 }, - { "col", 9 }, - { "colgroup", 10 }, - { "dd", 11 }, - { "details", 12 }, - { "dialog", 13 }, - { "dir", 14 }, - { "div", 15 }, - { "dl", 16 }, - { "dt", 17 }, - { "fieldset", 18 }, - { "figcaption", 19 }, - { "figure", 20 }, - { "footer", 21 }, - { "form", 22 }, - { "frame", 23 }, - { "frameset", 24 }, - { "h1", 25 }, - { "h2", 26 }, - { "h3", 27 }, - { "h4", 28 }, - { "h5", 29 }, - { "h6", 30 }, - { "head", 31 }, - { "header", 32 }, - { "hr", 33 }, - { "html", 34 }, - { "iframe", 35 }, - { "legend", 36 }, - { "li", 37 }, - { "link", 38 }, - { "main", 39 }, - { "menu", 40 }, - { "menuitem", 41 }, - { "nav", 42 }, - { "noframes", 43 }, - { "ol", 44 }, - { "optgroup", 45 }, - { "option", 46 }, - { "p", 47 }, - { "param", 48 }, - { "pre", 49 }, // <=== special group 1 - { "script", 50 }, // <=== special group 1 - { "section", 51 }, - { "source", 52 }, - { "style", 53 }, // <=== special group 1 - { "summary", 54 }, - { "table", 55 }, - { "textarea", 56 }, // <=== special group 1 - { "tbody", 57 }, - { "td", 58 }, - { "tfoot", 59 }, - { "th", 60 }, - { "thead", 61 }, - { "title", 62 }, - { "tr", 63 }, - { "track", 64 }, - { "ul", 65 } + 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), }; + + if (state.TrackTrivia) + { + htmlBlock.LinesBefore = state.UseLinesBefore(); + htmlBlock.NewLine = state.Line.NewLine; + } + + state.NewBlocks.Push(htmlBlock); + return BlockState.Continue; } + + private static readonly CompactPrefixTree HtmlTags = new(66, 94, 83) + { + { "address", 0 }, + { "article", 1 }, + { "aside", 2 }, + { "base", 3 }, + { "basefont", 4 }, + { "blockquote", 5 }, + { "body", 6 }, + { "caption", 7 }, + { "center", 8 }, + { "col", 9 }, + { "colgroup", 10 }, + { "dd", 11 }, + { "details", 12 }, + { "dialog", 13 }, + { "dir", 14 }, + { "div", 15 }, + { "dl", 16 }, + { "dt", 17 }, + { "fieldset", 18 }, + { "figcaption", 19 }, + { "figure", 20 }, + { "footer", 21 }, + { "form", 22 }, + { "frame", 23 }, + { "frameset", 24 }, + { "h1", 25 }, + { "h2", 26 }, + { "h3", 27 }, + { "h4", 28 }, + { "h5", 29 }, + { "h6", 30 }, + { "head", 31 }, + { "header", 32 }, + { "hr", 33 }, + { "html", 34 }, + { "iframe", 35 }, + { "legend", 36 }, + { "li", 37 }, + { "link", 38 }, + { "main", 39 }, + { "menu", 40 }, + { "menuitem", 41 }, + { "nav", 42 }, + { "noframes", 43 }, + { "ol", 44 }, + { "optgroup", 45 }, + { "option", 46 }, + { "p", 47 }, + { "param", 48 }, + { "pre", 49 }, // <=== special group 1 + { "script", 50 }, // <=== special group 1 + { "section", 51 }, + { "source", 52 }, + { "style", 53 }, // <=== special group 1 + { "summary", 54 }, + { "table", 55 }, + { "textarea", 56 }, // <=== special group 1 + { "tbody", 57 }, + { "td", 58 }, + { "tfoot", 59 }, + { "th", 60 }, + { "thead", 61 }, + { "title", 62 }, + { "tr", 63 }, + { "track", 64 }, + { "ul", 65 } + }; } \ No newline at end of file diff --git a/src/Markdig/Parsers/IAttributesParseable.cs b/src/Markdig/Parsers/IAttributesParseable.cs index 59269ad89..e09ed609c 100644 --- a/src/Markdig/Parsers/IAttributesParseable.cs +++ b/src/Markdig/Parsers/IAttributesParseable.cs @@ -5,26 +5,25 @@ using Markdig.Helpers; using Markdig.Syntax; -namespace Markdig.Parsers -{ - /// - /// A delegates that allows to process attached attributes at time. - /// - /// The processor. - /// The slice to look for attached attributes. - /// The block. - /// true if attributes were found; otherwise false - public delegate bool TryParseAttributesDelegate( - BlockProcessor processor, ref StringSlice slice, IBlock block); +namespace Markdig.Parsers; +/// +/// A delegates that allows to process attached attributes at time. +/// +/// The processor. +/// The slice to look for attached attributes. +/// The block. +/// true if attributes were found; otherwise false +public delegate bool TryParseAttributesDelegate( + BlockProcessor processor, ref StringSlice slice, IBlock block); + +/// +/// An interface used to tag that supports parsing +/// +public interface IAttributesParseable +{ /// - /// An interface used to tag that supports parsing + /// A delegates that allows to process attached attributes /// - public interface IAttributesParseable - { - /// - /// A delegates that allows to process attached attributes - /// - TryParseAttributesDelegate? TryParseAttributes { get; set; } - } + TryParseAttributesDelegate? TryParseAttributes { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Parsers/IBlockParser.cs b/src/Markdig/Parsers/IBlockParser.cs index 527b87e49..8305a5324 100644 --- a/src/Markdig/Parsers/IBlockParser.cs +++ b/src/Markdig/Parsers/IBlockParser.cs @@ -4,44 +4,43 @@ using Markdig.Syntax; -namespace Markdig.Parsers +namespace Markdig.Parsers; + +/// +/// Base interface for a . +/// +/// +/// +public interface IBlockParser : IMarkdownParser { /// - /// Base interface for a . + /// Determines whether this instance can interrupt the specified block being processed. /// - /// - /// - public interface IBlockParser : IMarkdownParser - { - /// - /// Determines whether this instance can interrupt the specified block being processed. - /// - /// The parser processor. - /// The block being processed. - /// true if this parser can interrupt the specified block being processed. - bool CanInterrupt(TProcessor processor, Block block); + /// The parser processor. + /// The block being processed. + /// true if this parser can interrupt the specified block being processed. + bool CanInterrupt(TProcessor processor, Block block); - /// - /// Tries to match a block opening. - /// - /// The parser processor. - /// The result of the match - BlockState TryOpen(TProcessor processor); + /// + /// Tries to match a block opening. + /// + /// The parser processor. + /// The result of the match + BlockState TryOpen(TProcessor processor); - /// - /// Tries to continue matching a block already opened. - /// - /// The parser processor. - /// The block already opened. - /// The result of the match. By default, don't expect any newline - BlockState TryContinue(TProcessor processor, Block block); + /// + /// Tries to continue matching a block already opened. + /// + /// The parser processor. + /// The block already opened. + /// The result of the match. By default, don't expect any newline + BlockState TryContinue(TProcessor processor, Block block); - /// - /// Called when a block matched by this parser is being closed (to allow final computation on the block). - /// - /// The parser processor. - /// The block being closed. - /// true to keep the block; false to remove it. True by default. - bool Close(TProcessor processor, Block block); - } + /// + /// Called when a block matched by this parser is being closed (to allow final computation on the block). + /// + /// The parser processor. + /// The block being closed. + /// true to keep the block; false to remove it. True by default. + bool Close(TProcessor processor, Block block); } \ No newline at end of file diff --git a/src/Markdig/Parsers/IInlineParser.cs b/src/Markdig/Parsers/IInlineParser.cs index 1a841a524..9a439a77e 100644 --- a/src/Markdig/Parsers/IInlineParser.cs +++ b/src/Markdig/Parsers/IInlineParser.cs @@ -4,21 +4,20 @@ using Markdig.Helpers; -namespace Markdig.Parsers +namespace Markdig.Parsers; + +/// +/// Base interface for parsing an . +/// +/// +/// +public interface IInlineParser : IMarkdownParser { /// - /// Base interface for parsing an . + /// Tries to match the specified slice. /// - /// - /// - public interface IInlineParser : IMarkdownParser - { - /// - /// Tries to match the specified slice. - /// - /// The parser processor. - /// The text slice. - /// true if this parser found a match; false otherwise - bool Match(InlineProcessor processor, ref StringSlice slice); - } + /// The parser processor. + /// The text slice. + /// true if this parser found a match; false otherwise + bool Match(InlineProcessor processor, ref StringSlice slice); } \ No newline at end of file diff --git a/src/Markdig/Parsers/IMarkdownParser.cs b/src/Markdig/Parsers/IMarkdownParser.cs index 53808a739..05b7db43d 100644 --- a/src/Markdig/Parsers/IMarkdownParser.cs +++ b/src/Markdig/Parsers/IMarkdownParser.cs @@ -2,27 +2,26 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -namespace Markdig.Parsers +namespace Markdig.Parsers; + +/// +/// Base interface for a block or inline parser. +/// +/// The type of processor. +public interface IMarkdownParser { /// - /// Base interface for a block or inline parser. + /// Gets the opening characters this parser will be triggered if the character is found. /// - /// The type of processor. - public interface IMarkdownParser - { - /// - /// Gets the opening characters this parser will be triggered if the character is found. - /// - char[]? OpeningCharacters { get; } + char[]? OpeningCharacters { get; } - /// - /// Initializes this parser with the specified parser processor. - /// - void Initialize(); + /// + /// Initializes this parser with the specified parser processor. + /// + void Initialize(); - /// - /// Gets the index of this parser in or . - /// - int Index { get; } - } + /// + /// Gets the index of this parser in or . + /// + int Index { get; } } \ No newline at end of file diff --git a/src/Markdig/Parsers/IPostInlineProcessor.cs b/src/Markdig/Parsers/IPostInlineProcessor.cs index 54d18f7b5..613d1ac0f 100644 --- a/src/Markdig/Parsers/IPostInlineProcessor.cs +++ b/src/Markdig/Parsers/IPostInlineProcessor.cs @@ -4,23 +4,22 @@ using Markdig.Syntax.Inlines; -namespace Markdig.Parsers +namespace Markdig.Parsers; + +/// +/// A processor called at the end of processing all inlines. +/// +public interface IPostInlineProcessor { /// - /// A procesor called at the end of processing all inlines. + /// Processes the delimiters. /// - public interface IPostInlineProcessor - { - /// - /// Processes the delimiters. - /// - /// The parser state. - /// The root inline. - /// The last child. - /// Index of this delimiter processor. - /// - /// true to continue to the next delimiter processor; - /// false to stop the process (in case a processor is performing sub-sequent processor itself) - bool PostProcess(InlineProcessor state, Inline? root, Inline? lastChild, int postInlineProcessorIndex, bool isFinalProcessing); - } + /// The parser state. + /// The root inline. + /// The last child. + /// Index of this delimiter processor. + /// + /// true to continue to the next delimiter processor; + /// false to stop the process (in case a processor is performing sub-sequent processor itself) + bool PostProcess(InlineProcessor state, Inline? root, Inline? lastChild, int postInlineProcessorIndex, bool isFinalProcessing); } \ No newline at end of file diff --git a/src/Markdig/Parsers/IndentedCodeBlockParser.cs b/src/Markdig/Parsers/IndentedCodeBlockParser.cs index a07c79318..de50da31f 100644 --- a/src/Markdig/Parsers/IndentedCodeBlockParser.cs +++ b/src/Markdig/Parsers/IndentedCodeBlockParser.cs @@ -4,150 +4,149 @@ using Markdig.Helpers; using Markdig.Syntax; -using System.Collections.Generic; + using static Markdig.Syntax.CodeBlock; -namespace Markdig.Parsers +namespace Markdig.Parsers; + +/// +/// Block parser for an indented . +/// +/// +public class IndentedCodeBlockParser : BlockParser { - /// - /// Block parser for an indented . - /// - /// - public class IndentedCodeBlockParser : BlockParser + public override bool CanInterrupt(BlockProcessor processor, Block block) { - public override bool CanInterrupt(BlockProcessor processor, Block block) - { - return !block.IsParagraphBlock; - } + return !block.IsParagraphBlock; + } - public override BlockState TryOpen(BlockProcessor processor) + public override BlockState TryOpen(BlockProcessor processor) + { + var result = TryContinue(processor, null); + if (result == BlockState.Continue) { - var result = TryContinue(processor, null); - if (result == BlockState.Continue) - { - // Save the column where we need to go back - var column = processor.Column; - var sourceStartPosition = processor.Start; + // Save the column where we need to go back + var column = processor.Column; + var sourceStartPosition = processor.Start; - // Unwind all indents all spaces before in order to calculate correct span - processor.UnwindAllIndents(); + // Unwind all indents all spaces before in order to calculate correct span + processor.UnwindAllIndents(); - var codeBlock = new CodeBlock(this) - { - Column = processor.Column, - Span = new SourceSpan(processor.Start, processor.Line.End), - }; + var codeBlock = new CodeBlock(this) + { + Column = processor.Column, + Span = new SourceSpan(processor.Start, processor.Line.End), + }; - if (processor.TrackTrivia) - { - codeBlock.LinesBefore = processor.UseLinesBefore(); - codeBlock.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) - }; - codeBlock.CodeBlockLines.Add(codeBlockLine); - processor.NewBlocks.Push(codeBlock); + var codeBlockLine = new CodeBlockLine + { + TriviaBefore = processor.UseTrivia(sourceStartPosition - 1) + }; + codeBlock.CodeBlockLines.Add(codeBlockLine); + processor.NewBlocks.Push(codeBlock); - // Go back to the correct column - processor.GoToColumn(column); - } - return result; + // Go back to the correct column + processor.GoToColumn(column); } + return result; + } - public override BlockState TryContinue(BlockProcessor processor, Block? block) + public override BlockState TryContinue(BlockProcessor processor, Block? block) + { + if (!processor.IsCodeIndent || processor.IsBlankLine) { - if (!processor.IsCodeIndent || processor.IsBlankLine) + if (block is null || !processor.IsBlankLine) { - if (block is null || !processor.IsBlankLine) + if (block != null) { - if (block != null) + var codeBlock = (CodeBlock)block; + // add trailing blank lines to blank lines stack of processor + for (int i = codeBlock.Lines.Count - 1; i >= 0; i--) { - var codeBlock = (CodeBlock)block; - // add trailing blank lines to blank lines stack of processor - for (int i = codeBlock.Lines.Count - 1; i >= 0; i--) + var line = codeBlock.Lines.Lines[i]; + if (line.Slice.IsEmpty) { - var line = codeBlock.Lines.Lines[i]; - if (line.Slice.IsEmpty) - { - codeBlock.Lines.RemoveAt(i); + codeBlock.Lines.RemoveAt(i); - if (processor.TrackTrivia) - { - processor.LinesBefore ??= new List(); - processor.LinesBefore.Add(line.Slice); - } - } - else + if (processor.TrackTrivia) { - break; + processor.LinesBefore ??= new List(); + processor.LinesBefore.Add(line.Slice); } } + else + { + break; + } } - return BlockState.None; } + return BlockState.None; } + } - // If we don't have a blank line, we reset to the indent - if (processor.Indent > 4) - { - processor.GoToCodeIndent(); - } - if (block != null) - { - block.UpdateSpanEnd(processor.Line.End); + // If we don't have a blank line, we reset to the indent + if (processor.Indent > 4) + { + processor.GoToCodeIndent(); + } + if (block != null) + { + block.UpdateSpanEnd(processor.Line.End); - // lines - var cb = (CodeBlock)block; - var codeBlockLine = new CodeBlockLine(); + // lines + var cb = (CodeBlock)block; + var codeBlockLine = new CodeBlockLine(); - cb.CodeBlockLines.Add(codeBlockLine); + cb.CodeBlockLines.Add(codeBlockLine); - if (processor.TrackTrivia) - { - codeBlockLine.TriviaBefore = processor.UseTrivia(processor.Start - 1); - 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; } - public override bool Close(BlockProcessor processor, Block block) + return BlockState.Continue; + } + + public override bool Close(BlockProcessor processor, Block block) + { + var codeBlock = (CodeBlock)block; + if (codeBlock is null) { - var codeBlock = (CodeBlock)block; - if (codeBlock is null) - { - return true; - } + return true; + } - // Remove any trailing blankline - for (int i = codeBlock.Lines.Count - 1; i >= 0; i--) + // Remove any trailing blankline + for (int i = codeBlock.Lines.Count - 1; i >= 0; i--) + { + var line = codeBlock.Lines.Lines[i]; + if (line.Slice.IsEmpty) { - var line = codeBlock.Lines.Lines[i]; - if (line.Slice.IsEmpty) - { - codeBlock.Lines.RemoveAt(i); + codeBlock.Lines.RemoveAt(i); - // if there are newlines after an indented codeblock, we must transform them - // into empty lines after the block. as whitespace is stripped from the Line - // we get that back from the beforeWhitespace on the CodeBlockLine. - if (processor.TrackTrivia) - { - var quoteLine = codeBlock.CodeBlockLines[i]; - var emptyLine = new StringSlice(line.Slice.Text, quoteLine.TriviaBefore.Start, line.Slice.End, line.NewLine); - block.LinesAfter ??= new List(); - block.LinesAfter.Add(emptyLine); - } - } - else + // if there are newlines after an indented codeblock, we must transform them + // into empty lines after the block. as whitespace is stripped from the Line + // we get that back from the beforeWhitespace on the CodeBlockLine. + if (processor.TrackTrivia) { - break; + var quoteLine = codeBlock.CodeBlockLines[i]; + var emptyLine = new StringSlice(line.Slice.Text, quoteLine.TriviaBefore.Start, line.Slice.End, line.NewLine); + block.LinesAfter ??= new List(); + block.LinesAfter.Add(emptyLine); } } - return true; + else + { + break; + } } + return true; } } \ No newline at end of file diff --git a/src/Markdig/Parsers/InlineParser.cs b/src/Markdig/Parsers/InlineParser.cs index ace5dac7e..be5d3b02b 100644 --- a/src/Markdig/Parsers/InlineParser.cs +++ b/src/Markdig/Parsers/InlineParser.cs @@ -4,20 +4,19 @@ using Markdig.Helpers; -namespace Markdig.Parsers +namespace Markdig.Parsers; + +/// +/// Base class for parsing an . +/// +/// +public abstract class InlineParser : ParserBase, IInlineParser { /// - /// Base class for parsing an . + /// Tries to match the specified slice. /// - /// - public abstract class InlineParser : ParserBase, IInlineParser - { - /// - /// Tries to match the specified slice. - /// - /// The parser processor. - /// The text slice. - /// true if this parser found a match; false otherwise - public abstract bool Match(InlineProcessor processor, ref StringSlice slice); - } + /// The parser processor. + /// The text slice. + /// true if this parser found a match; false otherwise + public abstract bool Match(InlineProcessor processor, ref StringSlice slice); } \ No newline at end of file diff --git a/src/Markdig/Parsers/InlineParserList.cs b/src/Markdig/Parsers/InlineParserList.cs index e16032b88..0db69c966 100644 --- a/src/Markdig/Parsers/InlineParserList.cs +++ b/src/Markdig/Parsers/InlineParserList.cs @@ -2,33 +2,30 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using System.Collections.Generic; +namespace Markdig.Parsers; -namespace Markdig.Parsers +/// +/// A list of . +/// +/// +public class InlineParserList : ParserList { - /// - /// A list of . - /// - /// - public class InlineParserList : ParserList + public InlineParserList(IEnumerable parsers) : base(parsers) { - public InlineParserList(IEnumerable parsers) : base(parsers) + // Prepare the list of post inline processors + var postInlineProcessors = new List(); + foreach (var parser in this) { - // Prepare the list of post inline processors - var postInlineProcessors = new List(); - foreach (var parser in this) + if (parser is IPostInlineProcessor delimProcessor) { - if (parser is IPostInlineProcessor delimProcessor) - { - postInlineProcessors.Add(delimProcessor); - } + postInlineProcessors.Add(delimProcessor); } - PostInlineProcessors = postInlineProcessors.ToArray(); } - - /// - /// Gets the registered post inline processors. - /// - public IPostInlineProcessor[] PostInlineProcessors { get; private set; } + PostInlineProcessors = postInlineProcessors.ToArray(); } + + /// + /// Gets the registered post inline processors. + /// + public IPostInlineProcessor[] PostInlineProcessors { get; private set; } } \ No newline at end of file diff --git a/src/Markdig/Parsers/InlineProcessor.cs b/src/Markdig/Parsers/InlineProcessor.cs index 6b6b046fe..7e3a3f8b8 100644 --- a/src/Markdig/Parsers/InlineProcessor.cs +++ b/src/Markdig/Parsers/InlineProcessor.cs @@ -2,441 +2,439 @@ // 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.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; + using Markdig.Helpers; using Markdig.Parsers.Inlines; using Markdig.Syntax; using Markdig.Syntax.Inlines; -namespace Markdig.Parsers +namespace Markdig.Parsers; + +/// +/// A delegate called at inline processing stage. +/// +/// The processor. +/// The inline being processed. +public delegate void ProcessInlineDelegate(InlineProcessor processor, Inline? inline); + +/// +/// The inline parser state used by all . +/// +public class InlineProcessor { + private readonly List lineOffsets = new(); + private int previousSliceOffset; + private int previousLineIndexForSliceOffset; + + /// + /// Initializes a new instance of the class. + /// + /// The document. + /// The parsers. + /// A value indicating whether to provide precise source location. + /// A parser context used for the parsing. + /// Whether to parse trivia such as whitespace, extra heading characters and unescaped string values. + /// + /// + public InlineProcessor(MarkdownDocument document, InlineParserList parsers, bool preciseSourcelocation, MarkdownParserContext? context, bool trackTrivia = false) + { + Setup(document, parsers, preciseSourcelocation, context, trackTrivia); + } + + private InlineProcessor() { } + /// - /// A delegate called at inline processing stage. + /// Gets the current block being processed. /// - /// The processor. - /// The inline being processed. - public delegate void ProcessInlineDelegate(InlineProcessor processor, Inline? inline); + public LeafBlock? Block { get; private set; } /// - /// The inline parser state used by all . + /// Gets a value indicating whether to provide precise source location. /// - public class InlineProcessor + public bool PreciseSourceLocation { get; private set; } + + /// + /// Gets or sets the new block to replace the block being processed. + /// + public Block? BlockNew { get; set; } + + /// + /// Gets or sets the current inline. Used by to return a new inline if match was successfull + /// + public Inline? Inline { get; set; } + + /// + /// Gets the root container of the current . + /// + public ContainerInline? Root { get; internal set; } + + /// + /// Gets the list of inline parsers. + /// + public InlineParserList Parsers { get; private set; } = null!; // Set in Setup + + /// + /// Gets the parser context or null if none is available. + /// + public MarkdownParserContext? Context { get; private set; } + + /// + /// Gets the root document. + /// + public MarkdownDocument Document { get; private set; } = null!; // Set in Setup + + /// + /// Gets or sets the index of the line from the begining of the document being processed. + /// + public int LineIndex { get; private set; } + + /// + /// Gets the parser states that can be used by using their property. + /// + public object[] ParserStates { get; private set; } = null!; // Set in Setup + + /// + /// Gets or sets the debug log writer. No log if null. + /// + public TextWriter? DebugLog { get; set; } + + /// + /// True to parse trivia such as whitespace, extra heading characters and unescaped + /// string values. + /// + public bool TrackTrivia { get; private set; } + + /// + /// Gets the literal inline parser. + /// + public LiteralInlineParser LiteralInlineParser { get; } = new(); + + public SourceSpan GetSourcePositionFromLocalSpan(SourceSpan span) { - private readonly List lineOffsets = new(); - private int previousSliceOffset; - private int previousLineIndexForSliceOffset; - - /// - /// Initializes a new instance of the class. - /// - /// The document. - /// The parsers. - /// A value indicating whether to provide precise source location. - /// A parser context used for the parsing. - /// Whether to parse trivia such as whitespace, extra heading characters and unescaped string values. - /// - /// - public InlineProcessor(MarkdownDocument document, InlineParserList parsers, bool preciseSourcelocation, MarkdownParserContext? context, bool trackTrivia = false) + if (span.IsEmpty) { - Setup(document, parsers, preciseSourcelocation, context, trackTrivia); + return SourceSpan.Empty; } - private InlineProcessor() { } - - /// - /// Gets the current block being processed. - /// - public LeafBlock? Block { get; private set; } - - /// - /// Gets a value indicating whether to provide precise source location. - /// - public bool PreciseSourceLocation { get; private set; } - - /// - /// Gets or sets the new block to replace the block being processed. - /// - public Block? BlockNew { get; set; } - - /// - /// Gets or sets the current inline. Used by to return a new inline if match was successfull - /// - public Inline? Inline { get; set; } - - /// - /// Gets the root container of the current . - /// - public ContainerInline? Root { get; internal set; } - - /// - /// Gets the list of inline parsers. - /// - public InlineParserList Parsers { get; private set; } = null!; // Set in Setup - - /// - /// Gets the parser context or null if none is available. - /// - public MarkdownParserContext? Context { get; private set; } - - /// - /// Gets the root document. - /// - public MarkdownDocument Document { get; private set; } = null!; // Set in Setup - - /// - /// Gets or sets the index of the line from the begining of the document being processed. - /// - public int LineIndex { get; private set; } - - /// - /// Gets the parser states that can be used by using their property. - /// - public object[] ParserStates { get; private set; } = null!; // Set in Setup - - /// - /// Gets or sets the debug log writer. No log if null. - /// - public TextWriter? DebugLog { get; set; } - - /// - /// True to parse trivia such as whitespace, extra heading characters and unescaped - /// string values. - /// - public bool TrackTrivia { get; private set; } - - /// - /// Gets the literal inline parser. - /// - public LiteralInlineParser LiteralInlineParser { get; } = new(); - - public SourceSpan GetSourcePositionFromLocalSpan(SourceSpan span) - { - if (span.IsEmpty) - { - return SourceSpan.Empty; - } - - return new SourceSpan(GetSourcePosition(span.Start), GetSourcePosition(span.End)); - } + return new SourceSpan(GetSourcePosition(span.Start), GetSourcePosition(span.End)); + } - /// - /// Gets the source position for the specified offset within the current slice. - /// - /// The slice offset. - /// The line index. - /// The column. - /// The source position - public int GetSourcePosition(int sliceOffset, out int lineIndex, out int column) + /// + /// Gets the source position for the specified offset within the current slice. + /// + /// The slice offset. + /// The line index. + /// The column. + /// The source position + public int GetSourcePosition(int sliceOffset, out int lineIndex, out int column) + { + column = 0; + lineIndex = sliceOffset >= previousSliceOffset ? previousLineIndexForSliceOffset : 0; + int position = 0; + if (PreciseSourceLocation) { - column = 0; - lineIndex = sliceOffset >= previousSliceOffset ? previousLineIndexForSliceOffset : 0; - int position = 0; - if (PreciseSourceLocation) - { #if NET - var offsets = CollectionsMarshal.AsSpan(lineOffsets); + var offsets = CollectionsMarshal.AsSpan(lineOffsets); - for (; (uint)lineIndex < (uint)offsets.Length; lineIndex++) - { - ref var lineOffset = ref offsets[lineIndex]; + for (; (uint)lineIndex < (uint)offsets.Length; lineIndex++) + { + ref var lineOffset = ref offsets[lineIndex]; #else - for (; lineIndex < lineOffsets.Count; lineIndex++) - { - var lineOffset = lineOffsets[lineIndex]; + for (; lineIndex < lineOffsets.Count; lineIndex++) + { + var lineOffset = lineOffsets[lineIndex]; #endif - if (sliceOffset <= lineOffset.End) - { - // Use the beginning of the line as a previous slice offset - // (since it is on the same line) - previousSliceOffset = lineOffset.Start; - var delta = sliceOffset - previousSliceOffset; - column = lineOffset.Column + delta; - position = lineOffset.LinePosition + delta + lineOffset.Offset; - previousLineIndexForSliceOffset = lineIndex; - - // Return an absolute line index - lineIndex = lineIndex + LineIndex; - break; - } + if (sliceOffset <= lineOffset.End) + { + // Use the beginning of the line as a previous slice offset + // (since it is on the same line) + previousSliceOffset = lineOffset.Start; + var delta = sliceOffset - previousSliceOffset; + column = lineOffset.Column + delta; + position = lineOffset.LinePosition + delta + lineOffset.Offset; + previousLineIndexForSliceOffset = lineIndex; + + // Return an absolute line index + lineIndex = lineIndex + LineIndex; + break; } } - return position; } + return position; + } - /// - /// Gets the source position for the specified offset within the current slice. - /// - /// The slice offset. - /// The source position - public int GetSourcePosition(int sliceOffset) + /// + /// Gets the source position for the specified offset within the current slice. + /// + /// The slice offset. + /// The source position + public int GetSourcePosition(int sliceOffset) + { + if (PreciseSourceLocation) { - if (PreciseSourceLocation) - { - int lineIndex = sliceOffset >= previousSliceOffset ? previousLineIndexForSliceOffset : 0; + int lineIndex = sliceOffset >= previousSliceOffset ? previousLineIndexForSliceOffset : 0; #if NET - var offsets = CollectionsMarshal.AsSpan(lineOffsets); + var offsets = CollectionsMarshal.AsSpan(lineOffsets); - for (; (uint)lineIndex < (uint)offsets.Length; lineIndex++) - { - ref var lineOffset = ref offsets[lineIndex]; + for (; (uint)lineIndex < (uint)offsets.Length; lineIndex++) + { + ref var lineOffset = ref offsets[lineIndex]; #else - for (; lineIndex < lineOffsets.Count; lineIndex++) - { - var lineOffset = lineOffsets[lineIndex]; + for (; lineIndex < lineOffsets.Count; lineIndex++) + { + var lineOffset = lineOffsets[lineIndex]; #endif - if (sliceOffset <= lineOffset.End) - { - previousLineIndexForSliceOffset = lineIndex; - previousSliceOffset = lineOffset.Start; + if (sliceOffset <= lineOffset.End) + { + previousLineIndexForSliceOffset = lineIndex; + previousSliceOffset = lineOffset.Start; - return sliceOffset - lineOffset.Start + lineOffset.LinePosition + lineOffset.Offset; - } + return sliceOffset - lineOffset.Start + lineOffset.LinePosition + lineOffset.Offset; } } - return 0; } + return 0; + } - /// - /// Processes the inline of the specified . - /// - /// The leaf block. - public void ProcessInlineLeaf(LeafBlock leafBlock) + /// + /// Processes the inline of the specified . + /// + /// The leaf block. + public void ProcessInlineLeaf(LeafBlock leafBlock) + { + if (leafBlock is null) ThrowHelper.ArgumentNullException_leafBlock(); + + // clear parser states + Array.Clear(ParserStates, 0, ParserStates.Length); + + Root = new ContainerInline() { IsClosed = false }; + leafBlock.Inline = Root; + Inline = null; + Block = leafBlock; + BlockNew = null; + LineIndex = leafBlock.Line; + + previousSliceOffset = 0; + previousLineIndexForSliceOffset = 0; + lineOffsets.Clear(); + var text = leafBlock.Lines.ToSlice(lineOffsets); + leafBlock.Lines.Release(); + int previousStart = -1; + + while (!text.IsEmpty) { - if (leafBlock is null) ThrowHelper.ArgumentNullException_leafBlock(); - - // clear parser states - Array.Clear(ParserStates, 0, ParserStates.Length); - - Root = new ContainerInline() { IsClosed = false }; - leafBlock.Inline = Root; - Inline = null; - Block = leafBlock; - BlockNew = null; - LineIndex = leafBlock.Line; - - previousSliceOffset = 0; - previousLineIndexForSliceOffset = 0; - lineOffsets.Clear(); - var text = leafBlock.Lines.ToSlice(lineOffsets); - leafBlock.Lines.Release(); - int previousStart = -1; - - while (!text.IsEmpty) + // Security check so that the parser can't go into a crazy infinite loop if one extension is messing + if (previousStart == text.Start) { - // Security check so that the parser can't go into a crazy infinite loop if one extension is messing - if (previousStart == text.Start) - { - ThrowHelper.InvalidOperationException($"The parser is in an invalid infinite loop while trying to parse inlines for block [{leafBlock.GetType().Name}] at position ({leafBlock.ToPositionText()}"); - } - previousStart = text.Start; + ThrowHelper.InvalidOperationException($"The parser is in an invalid infinite loop while trying to parse inlines for block [{leafBlock.GetType().Name}] at position ({leafBlock.ToPositionText()}"); + } + previousStart = text.Start; - var c = text.CurrentChar; + var c = text.CurrentChar; - var textSaved = text; - var parsers = Parsers.GetParsersForOpeningCharacter(c); - if (parsers != null) + var textSaved = text; + var parsers = Parsers.GetParsersForOpeningCharacter(c); + if (parsers != null) + { + for (int i = 0; i < parsers.Length; i++) { - for (int i = 0; i < parsers.Length; i++) + text = textSaved; + if (parsers[i].Match(this, ref text)) { - text = textSaved; - if (parsers[i].Match(this, ref text)) - { - goto done; - } + goto done; } } - parsers = Parsers.GlobalParsers; - if (parsers != null) + } + parsers = Parsers.GlobalParsers; + if (parsers != null) + { + for (int i = 0; i < parsers.Length; i++) { - for (int i = 0; i < parsers.Length; i++) + text = textSaved; + if (parsers[i].Match(this, ref text)) { - text = textSaved; - if (parsers[i].Match(this, ref text)) - { - goto done; - } + goto done; } } + } - text = textSaved; - // Else match using the default literal inline parser - LiteralInlineParser.Match(this, ref text); + text = textSaved; + // Else match using the default literal inline parser + LiteralInlineParser.Match(this, ref text); - done: - var nextInline = Inline; - if (nextInline != null) + done: + var nextInline = Inline; + if (nextInline != null) + { + if (nextInline.Parent is null) { - if (nextInline.Parent is null) + // Get deepest container + var container = FindLastContainer(); + if (!ReferenceEquals(container, nextInline)) { - // Get deepest container - var container = FindLastContainer(); - if (!ReferenceEquals(container, nextInline)) - { - container.AppendChild(nextInline); - } + container.AppendChild(nextInline); + } - if (container == Root) + if (container == Root) + { + if (container.Span.IsEmpty) { - if (container.Span.IsEmpty) - { - container.Span = nextInline.Span; - } - container.Span.End = nextInline.Span.End; + container.Span = nextInline.Span; } - + container.Span.End = nextInline.Span.End; } - } - else - { - // Get deepest container - var container = FindLastContainer(); - Inline = container.LastChild is LeafInline ? container.LastChild : container; - if (Inline == Root) - { - Inline = null; - } } - - //if (DebugLog != null) - //{ - // DebugLog.WriteLine($"** Dump: char '{c}"); - // leafBlock.Inline.DumpTo(DebugLog); - //} } - - if (TrackTrivia) + else { - if (!(leafBlock is HeadingBlock)) + // Get deepest container + var container = FindLastContainer(); + + Inline = container.LastChild is LeafInline ? container.LastChild : container; + if (Inline == Root) { - var newLine = leafBlock.NewLine; - if (newLine != NewLine.None) - { - leafBlock.Inline.AppendChild(new LineBreakInline { NewLine = newLine }); - } + Inline = null; } } - Inline = null; - //if (DebugLog != null) - //{ - // DebugLog.WriteLine("** Dump before Emphasis:"); - // leafBlock.Inline.DumpTo(DebugLog); - //} - - // PostProcess all inlines - PostProcessInlines(0, Root, null, true); - - //TransformDelimitersToLiterals(); - //if (DebugLog != null) //{ - // DebugLog.WriteLine(); - // DebugLog.WriteLine("** Dump after Emphasis:"); + // DebugLog.WriteLine($"** Dump: char '{c}"); // leafBlock.Inline.DumpTo(DebugLog); //} } - public void PostProcessInlines(int startingIndex, Inline? root, Inline? lastChild, bool isFinalProcessing) + if (TrackTrivia) { - for (int i = startingIndex; i < Parsers.PostInlineProcessors.Length; i++) + if (!(leafBlock is HeadingBlock)) { - var postInlineProcessor = Parsers.PostInlineProcessors[i]; - if (!postInlineProcessor.PostProcess(this, root, lastChild, i, isFinalProcessing)) + var newLine = leafBlock.NewLine; + if (newLine != NewLine.None) { - break; + leafBlock.Inline.AppendChild(new LineBreakInline { NewLine = newLine }); } } } - private ContainerInline FindLastContainer() - { - var container = Block!.Inline!; - for (int depth = 0; ; depth++) - { - Inline? lastChild = container.LastChild; - if (lastChild is not null && lastChild.IsContainerInline && !lastChild.IsClosed) - { - container = Unsafe.As(lastChild); - } - else - { - ThrowHelper.CheckDepthLimit(depth, useLargeLimit: true); - return container; - } - } - } + Inline = null; + //if (DebugLog != null) + //{ + // DebugLog.WriteLine("** Dump before Emphasis:"); + // leafBlock.Inline.DumpTo(DebugLog); + //} + // PostProcess all inlines + PostProcessInlines(0, Root, null, true); - [MemberNotNull(nameof(Document), nameof(Parsers), nameof(ParserStates))] - private void Setup(MarkdownDocument document, InlineParserList parsers, bool preciseSourcelocation, MarkdownParserContext? context, bool trackTrivia) - { - if (document is null) ThrowHelper.ArgumentNullException(nameof(document)); - if (parsers is null) ThrowHelper.ArgumentNullException(nameof(parsers)); + //TransformDelimitersToLiterals(); - Document = document; - Parsers = parsers; - Context = context; - PreciseSourceLocation = preciseSourcelocation; - TrackTrivia = trackTrivia; + //if (DebugLog != null) + //{ + // DebugLog.WriteLine(); + // DebugLog.WriteLine("** Dump after Emphasis:"); + // leafBlock.Inline.DumpTo(DebugLog); + //} + } - if (ParserStates is null || ParserStates.Length < Parsers.Count) + public void PostProcessInlines(int startingIndex, Inline? root, Inline? lastChild, bool isFinalProcessing) + { + for (int i = startingIndex; i < Parsers.PostInlineProcessors.Length; i++) + { + var postInlineProcessor = Parsers.PostInlineProcessors[i]; + if (!postInlineProcessor.PostProcess(this, root, lastChild, i, isFinalProcessing)) { - ParserStates = new object[Parsers.Count]; + break; } } + } - private void Reset() + private ContainerInline FindLastContainer() + { + var container = Block!.Inline!; + for (int depth = 0; ; depth++) { - Block = null; - BlockNew = null; - Inline = null; - Root = null; - Parsers = null!; - Context = null; - Document = null!; - DebugLog = null; - - PreciseSourceLocation = false; - TrackTrivia = false; - - LineIndex = 0; - previousSliceOffset = 0; - previousLineIndexForSliceOffset = 0; - - LiteralInlineParser.PostMatch = null; - - lineOffsets.Clear(); - Array.Clear(ParserStates, 0, ParserStates.Length); + Inline? lastChild = container.LastChild; + if (lastChild is not null && lastChild.IsContainerInline && !lastChild.IsClosed) + { + container = Unsafe.As(lastChild); + } + else + { + ThrowHelper.CheckDepthLimit(depth, useLargeLimit: true); + return container; + } } + } - private static readonly InlineProcessorCache _cache = new(); - internal static InlineProcessor Rent(MarkdownDocument document, InlineParserList parsers, bool preciseSourcelocation, MarkdownParserContext? context, bool trackTrivia) - { - var processor = _cache.Get(); - processor.Setup(document, parsers, preciseSourcelocation, context, trackTrivia); - return processor; - } + [MemberNotNull(nameof(Document), nameof(Parsers), nameof(ParserStates))] + private void Setup(MarkdownDocument document, InlineParserList parsers, bool preciseSourcelocation, MarkdownParserContext? context, bool trackTrivia) + { + if (document is null) ThrowHelper.ArgumentNullException(nameof(document)); + if (parsers is null) ThrowHelper.ArgumentNullException(nameof(parsers)); + + Document = document; + Parsers = parsers; + Context = context; + PreciseSourceLocation = preciseSourcelocation; + TrackTrivia = trackTrivia; - internal static void Release(InlineProcessor processor) + if (ParserStates is null || ParserStates.Length < Parsers.Count) { - _cache.Release(processor); + ParserStates = new object[Parsers.Count]; } + } - private sealed class InlineProcessorCache : ObjectCache - { - protected override InlineProcessor NewInstance() => new InlineProcessor(); + private void Reset() + { + Block = null; + BlockNew = null; + Inline = null; + Root = null; + Parsers = null!; + Context = null; + Document = null!; + DebugLog = null; + + PreciseSourceLocation = false; + TrackTrivia = false; + + LineIndex = 0; + previousSliceOffset = 0; + previousLineIndexForSliceOffset = 0; + + LiteralInlineParser.PostMatch = null; + + lineOffsets.Clear(); + Array.Clear(ParserStates, 0, ParserStates.Length); + } - protected override void Reset(InlineProcessor instance) => instance.Reset(); - } + private static readonly InlineProcessorCache _cache = new(); + + internal static InlineProcessor Rent(MarkdownDocument document, InlineParserList parsers, bool preciseSourcelocation, MarkdownParserContext? context, bool trackTrivia) + { + var processor = _cache.Get(); + processor.Setup(document, parsers, preciseSourcelocation, context, trackTrivia); + return processor; + } + + internal static void Release(InlineProcessor processor) + { + _cache.Release(processor); + } + + private sealed class InlineProcessorCache : ObjectCache + { + protected override InlineProcessor NewInstance() => new InlineProcessor(); + + protected override void Reset(InlineProcessor instance) => instance.Reset(); } } \ No newline at end of file diff --git a/src/Markdig/Parsers/Inlines/AutolinkInlineParser.cs b/src/Markdig/Parsers/Inlines/AutolinkInlineParser.cs index c37929a45..79ce9f6fd 100644 --- a/src/Markdig/Parsers/Inlines/AutolinkInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/AutolinkInlineParser.cs @@ -6,64 +6,63 @@ using Markdig.Syntax; using Markdig.Syntax.Inlines; -namespace Markdig.Parsers.Inlines +namespace Markdig.Parsers.Inlines; + +/// +/// An inline parser for parsing . +/// +/// +public class AutolinkInlineParser : InlineParser { /// - /// An inline parser for parsing . + /// Initializes a new instance of the class. /// - /// - public class AutolinkInlineParser : InlineParser + public AutolinkInlineParser() { - /// - /// Initializes a new instance of the class. - /// - public AutolinkInlineParser() - { - OpeningCharacters = new[] {'<'}; - EnableHtmlParsing = true; - } + OpeningCharacters = new[] {'<'}; + EnableHtmlParsing = true; + } - /// - /// Gets or sets a value indicating whether to enable HTML parsing. Default is true - /// - public bool EnableHtmlParsing { get; set; } + /// + /// Gets or sets a value indicating whether to enable HTML parsing. Default is true + /// + public bool EnableHtmlParsing { get; set; } - public override bool Match(InlineProcessor processor, ref StringSlice slice) + public override bool Match(InlineProcessor processor, ref StringSlice slice) + { + var saved = slice; + int line; + int column; + if (LinkHelper.TryParseAutolink(ref slice, out string? link, out bool isEmail)) { - var saved = slice; - int line; - int column; - if (LinkHelper.TryParseAutolink(ref slice, out string? link, out bool isEmail)) - { - processor.Inline = new AutolinkInline(link) - { - IsEmail = isEmail, - Span = new SourceSpan(processor.GetSourcePosition(saved.Start, out line, out column), processor.GetSourcePosition(slice.Start - 1)), - Line = line, - Column = column - }; - } - else if (EnableHtmlParsing) + processor.Inline = new AutolinkInline(link) { - slice = saved; - if (!HtmlHelper.TryParseHtmlTag(ref slice, out string? htmlTag)) - { - return false; - } - - processor.Inline = new HtmlInline(htmlTag) - { - Span = new SourceSpan(processor.GetSourcePosition(saved.Start, out line, out column), processor.GetSourcePosition(slice.Start - 1)), - Line = line, - Column = column - }; - } - else + IsEmail = isEmail, + Span = new SourceSpan(processor.GetSourcePosition(saved.Start, out line, out column), processor.GetSourcePosition(slice.Start - 1)), + Line = line, + Column = column + }; + } + else if (EnableHtmlParsing) + { + slice = saved; + if (!HtmlHelper.TryParseHtmlTag(ref slice, out string? htmlTag)) { return false; } - return true; + processor.Inline = new HtmlInline(htmlTag) + { + Span = new SourceSpan(processor.GetSourcePosition(saved.Start, out line, out column), processor.GetSourcePosition(slice.Start - 1)), + Line = line, + Column = column + }; + } + else + { + return false; } + + return true; } } \ No newline at end of file diff --git a/src/Markdig/Parsers/Inlines/CodeInlineParser.cs b/src/Markdig/Parsers/Inlines/CodeInlineParser.cs index da6474f85..e53d2b9a4 100644 --- a/src/Markdig/Parsers/Inlines/CodeInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/CodeInlineParser.cs @@ -2,144 +2,143 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. +using System.Diagnostics; + using Markdig.Helpers; using Markdig.Syntax; using Markdig.Syntax.Inlines; -using System; -using System.Diagnostics; -namespace Markdig.Parsers.Inlines +namespace Markdig.Parsers.Inlines; + +/// +/// An inline parser for a . +/// +/// +public class CodeInlineParser : InlineParser { /// - /// An inline parser for a . + /// Initializes a new instance of the class. /// - /// - public class CodeInlineParser : InlineParser + public CodeInlineParser() + { + OpeningCharacters = new[] { '`' }; + } + + public override bool Match(InlineProcessor processor, ref StringSlice slice) { - /// - /// Initializes a new instance of the class. - /// - public CodeInlineParser() + var match = slice.CurrentChar; + if (slice.PeekCharExtra(-1) == match) { - OpeningCharacters = new[] { '`' }; + return false; } - public override bool Match(InlineProcessor processor, ref StringSlice slice) - { - var match = slice.CurrentChar; - if (slice.PeekCharExtra(-1) == match) - { - return false; - } + var startPosition = slice.Start; - var startPosition = slice.Start; + // Match the opened sticks + int openSticks = slice.CountAndSkipChar(match); + int contentStart = slice.Start; + int closeSticks = 0; - // Match the opened sticks - int openSticks = slice.CountAndSkipChar(match); - int contentStart = slice.Start; - int closeSticks = 0; + char c = slice.CurrentChar; - char c = slice.CurrentChar; + var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); - 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. + // The contents of the code span are the characters between the two backtick strings, normalized in the following ways: - // 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. - // The contents of the code span are the characters between the two backtick strings, normalized in the following ways: + // 1. line endings are converted to spaces. - // 1. line endings are converted to spaces. + // 2. If the resulting string both begins AND ends with a space character, but does not consist entirely + // of space characters, a single space character is removed from the front and back. + // This allows you to include code that begins or ends with backtick characters, which must be separated by + // whitespace from the opening or closing backtick strings. - // 2. If the resulting string both begins AND ends with a space character, but does not consist entirely - // of space characters, a single space character is removed from the front and back. - // This allows you to include code that begins or ends with backtick characters, which must be separated by - // whitespace from the opening or closing backtick strings. + bool allSpace = true; + bool containsNewLine = false; + var contentEnd = -1; - bool allSpace = true; - bool containsNewLine = false; - var contentEnd = -1; + while (c != '\0') + { + // Transform '\n' into a single space + if (c == '\n') + { + containsNewLine = true; + c = ' '; + } + else if (c == '\r') + { + containsNewLine = true; + slice.SkipChar(); + c = slice.CurrentChar; + continue; + } - while (c != '\0') + if (c == match) { - // Transform '\n' into a single space - if (c == '\n') - { - containsNewLine = true; - c = ' '; - } - else if (c == '\r') + contentEnd = slice.Start; + closeSticks = slice.CountAndSkipChar(match); + + if (openSticks == closeSticks) { - containsNewLine = true; - slice.SkipChar(); - c = slice.CurrentChar; - continue; + break; } - if (c == match) + allSpace = false; + builder.Append(match, closeSticks); + c = slice.CurrentChar; + } + else + { + builder.Append(c); + if (c != ' ') { - contentEnd = slice.Start; - closeSticks = slice.CountAndSkipChar(match); - - if (openSticks == closeSticks) - { - break; - } - allSpace = false; - builder.Append(match, closeSticks); - c = slice.CurrentChar; - } - else - { - builder.Append(c); - if (c != ' ') - { - allSpace = false; - } - c = slice.NextChar(); } + c = slice.NextChar(); } + } - bool isMatching = false; - if (closeSticks == openSticks) - { - ReadOnlySpan contentSpan = builder.AsSpan(); + bool isMatching = false; + if (closeSticks == openSticks) + { + ReadOnlySpan contentSpan = builder.AsSpan(); - var content = containsNewLine - ? new LazySubstring(contentSpan.ToString()) - : new LazySubstring(slice.Text, contentStart, contentSpan.Length); + var content = containsNewLine + ? new LazySubstring(contentSpan.ToString()) + : new LazySubstring(slice.Text, contentStart, contentSpan.Length); - Debug.Assert(contentSpan.SequenceEqual(content.AsSpan())); + Debug.Assert(contentSpan.SequenceEqual(content.AsSpan())); - // Remove one space from front and back if the string is not all spaces - if (!allSpace && contentSpan.Length > 2 && contentSpan[0] == ' ' && contentSpan[contentSpan.Length - 1] == ' ') - { - content.Offset++; - content.Length -= 2; - } - - int delimiterCount = Math.Min(openSticks, closeSticks); - var spanStart = processor.GetSourcePosition(startPosition, out int line, out int column); - var spanEnd = processor.GetSourcePosition(slice.Start - 1); - var codeInline = new CodeInline(content) - { - Delimiter = match, - Span = new SourceSpan(spanStart, spanEnd), - Line = line, - Column = column, - DelimiterCount = delimiterCount, - }; - - if (processor.TrackTrivia) - { - codeInline.ContentWithTrivia = new StringSlice(slice.Text, contentStart, contentEnd - 1); - } + // Remove one space from front and back if the string is not all spaces + if (!allSpace && contentSpan.Length > 2 && contentSpan[0] == ' ' && contentSpan[contentSpan.Length - 1] == ' ') + { + content.Offset++; + content.Length -= 2; + } - processor.Inline = codeInline; - isMatching = true; + int delimiterCount = Math.Min(openSticks, closeSticks); + var spanStart = processor.GetSourcePosition(startPosition, out int line, out int column); + var spanEnd = processor.GetSourcePosition(slice.Start - 1); + var codeInline = new CodeInline(content) + { + Delimiter = match, + Span = new SourceSpan(spanStart, spanEnd), + Line = line, + Column = column, + DelimiterCount = delimiterCount, + }; + + if (processor.TrackTrivia) + { + codeInline.ContentWithTrivia = new StringSlice(slice.Text, contentStart, contentEnd - 1); } - builder.Dispose(); - return isMatching; + processor.Inline = codeInline; + isMatching = true; } + + builder.Dispose(); + return isMatching; } } \ No newline at end of file diff --git a/src/Markdig/Parsers/Inlines/EmphasisDescriptor.cs b/src/Markdig/Parsers/Inlines/EmphasisDescriptor.cs index 5bf02152e..c2e8259e6 100644 --- a/src/Markdig/Parsers/Inlines/EmphasisDescriptor.cs +++ b/src/Markdig/Parsers/Inlines/EmphasisDescriptor.cs @@ -4,50 +4,49 @@ using Markdig.Helpers; -namespace Markdig.Parsers.Inlines +namespace Markdig.Parsers.Inlines; + +/// +/// Descriptor for an emphasis. +/// +public sealed class EmphasisDescriptor { /// - /// Descriptor for an emphasis. + /// Initializes a new instance of the class. /// - public sealed class EmphasisDescriptor + /// The character used for this emphasis. + /// The minimum number of character. + /// The maximum number of characters. + /// if set to true the emphasis can be used inside a word. + public EmphasisDescriptor(char character, int minimumCount, int maximumCount, bool enableWithinWord) { - /// - /// Initializes a new instance of the class. - /// - /// The character used for this emphasis. - /// The minimum number of character. - /// The maximum number of characters. - /// if set to true the emphasis can be used inside a word. - public EmphasisDescriptor(char character, int minimumCount, int maximumCount, bool enableWithinWord) - { - if (minimumCount < 1) ThrowHelper.ArgumentOutOfRangeException(nameof(minimumCount), "minimumCount must be >= 1"); - if (maximumCount < 1) ThrowHelper.ArgumentOutOfRangeException(nameof(maximumCount), "maximumCount must be >= 1"); - if (minimumCount > maximumCount) ThrowHelper.ArgumentOutOfRangeException(nameof(minimumCount), "minimumCount must be <= maximumCount"); - - Character = character; - MinimumCount = minimumCount; - MaximumCount = maximumCount; - EnableWithinWord = enableWithinWord; - } - - /// - /// The character of this emphasis. - /// - public char Character { get; } - - /// - /// The minimum number of character this emphasis is expected to have (must be >=1) - /// - public int MinimumCount { get; } - - /// - /// The maximum number of character this emphasis is expected to have (must be >=1 and >= minimumCount) - /// - public int MaximumCount { get; } - - /// - /// This emphasis can be used within a word. - /// - public bool EnableWithinWord { get; } + if (minimumCount < 1) ThrowHelper.ArgumentOutOfRangeException(nameof(minimumCount), "minimumCount must be >= 1"); + if (maximumCount < 1) ThrowHelper.ArgumentOutOfRangeException(nameof(maximumCount), "maximumCount must be >= 1"); + if (minimumCount > maximumCount) ThrowHelper.ArgumentOutOfRangeException(nameof(minimumCount), "minimumCount must be <= maximumCount"); + + Character = character; + MinimumCount = minimumCount; + MaximumCount = maximumCount; + EnableWithinWord = enableWithinWord; } + + /// + /// The character of this emphasis. + /// + public char Character { get; } + + /// + /// The minimum number of character this emphasis is expected to have (must be >=1) + /// + public int MinimumCount { get; } + + /// + /// The maximum number of character this emphasis is expected to have (must be >=1 and >= minimumCount) + /// + public int MaximumCount { get; } + + /// + /// This emphasis can be used within a word. + /// + public bool EnableWithinWord { get; } } \ No newline at end of file diff --git a/src/Markdig/Parsers/Inlines/EmphasisInlineParser.cs b/src/Markdig/Parsers/Inlines/EmphasisInlineParser.cs index 623b71a6d..11915dba7 100644 --- a/src/Markdig/Parsers/Inlines/EmphasisInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/EmphasisInlineParser.cs @@ -2,420 +2,418 @@ // 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.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; + using Markdig.Helpers; using Markdig.Renderers.Html; using Markdig.Syntax; using Markdig.Syntax.Inlines; -namespace Markdig.Parsers.Inlines +namespace Markdig.Parsers.Inlines; + +/// +/// An inline parser for . +/// +/// +/// +public class EmphasisInlineParser : InlineParser, IPostInlineProcessor { + private CharacterMap? emphasisMap; + private readonly DelimitersObjectCache inlinesCache = new DelimitersObjectCache(); + + [Obsolete("Use TryCreateEmphasisInlineDelegate instead", error: false)] + public delegate EmphasisInline CreateEmphasisInlineDelegate(char emphasisChar, bool isStrong); + public delegate EmphasisInline? TryCreateEmphasisInlineDelegate(char emphasisChar, int delimiterCount); + /// - /// An inline parser for . + /// Initializes a new instance of the class. /// - /// - /// - public class EmphasisInlineParser : InlineParser, IPostInlineProcessor + public EmphasisInlineParser() { - private CharacterMap? emphasisMap; - private readonly DelimitersObjectCache inlinesCache = new DelimitersObjectCache(); + EmphasisDescriptors = new List() + { + new EmphasisDescriptor('*', 1, 2, true), + new EmphasisDescriptor('_', 1, 2, false) + }; + } - [Obsolete("Use TryCreateEmphasisInlineDelegate instead", error: false)] - public delegate EmphasisInline CreateEmphasisInlineDelegate(char emphasisChar, bool isStrong); - public delegate EmphasisInline? TryCreateEmphasisInlineDelegate(char emphasisChar, int delimiterCount); + /// + /// Gets the emphasis descriptors. + /// + public List EmphasisDescriptors { get; } - /// - /// Initializes a new instance of the class. - /// - public EmphasisInlineParser() + /// + /// Determines whether this parser is using the specified character as an emphasis delimiter. + /// + /// The character to look for. + /// true if this parser is using the specified character as an emphasis delimiter; otherwise false + public bool HasEmphasisChar(char c) + { + foreach (var emphasis in EmphasisDescriptors) { - EmphasisDescriptors = new List() + if (emphasis.Character == c) { - new EmphasisDescriptor('*', 1, 2, true), - new EmphasisDescriptor('_', 1, 2, false) - }; + return true; + } } + return false; + } + + /// + /// Gets or sets the create emphasis inline delegate (allowing to create a different emphasis inline class) + /// + [Obsolete("Use TryCreateEmphasisInlineList instead", error: false)] + public CreateEmphasisInlineDelegate? CreateEmphasisInline { get; set; } + public readonly List TryCreateEmphasisInlineList = new List(); + + public override void Initialize() + { + OpeningCharacters = new char[EmphasisDescriptors.Count]; - /// - /// Gets the emphasis descriptors. - /// - public List EmphasisDescriptors { get; } - - /// - /// Determines whether this parser is using the specified character as an emphasis delimiter. - /// - /// The character to look for. - /// true if this parser is using the specified character as an emphasis delimiter; otherwise false - public bool HasEmphasisChar(char c) + var tempMap = new List>(); + for (int i = 0; i < EmphasisDescriptors.Count; i++) { - foreach (var emphasis in EmphasisDescriptors) + var emphasis = EmphasisDescriptors[i]; + if (Array.IndexOf(OpeningCharacters, emphasis.Character) >= 0) { - if (emphasis.Character == c) - { - return true; - } + ThrowHelper.InvalidOperationException( + $"The character `{emphasis.Character}` is already used by another emphasis descriptor"); } - return false; - } - /// - /// Gets or sets the create emphasis inline delegate (allowing to create a different emphasis inline class) - /// - [Obsolete("Use TryCreateEmphasisInlineList instead", error: false)] - public CreateEmphasisInlineDelegate? CreateEmphasisInline { get; set; } - public readonly List TryCreateEmphasisInlineList = new List(); + OpeningCharacters[i] = emphasis.Character; - public override void Initialize() - { - OpeningCharacters = new char[EmphasisDescriptors.Count]; + tempMap.Add(new KeyValuePair(emphasis.Character, emphasis)); + } - var tempMap = new List>(); - for (int i = 0; i < EmphasisDescriptors.Count; i++) - { - var emphasis = EmphasisDescriptors[i]; - if (Array.IndexOf(OpeningCharacters, emphasis.Character) >= 0) - { - ThrowHelper.InvalidOperationException( - $"The character `{emphasis.Character}` is already used by another emphasis descriptor"); - } + emphasisMap = new CharacterMap(tempMap); + } - OpeningCharacters[i] = emphasis.Character; + public bool PostProcess(InlineProcessor state, Inline? root, Inline? lastChild, int postInlineProcessorIndex, bool isFinalProcessing) + { + if (root is null || !root.IsContainerInline) + { + return true; + } - tempMap.Add(new KeyValuePair(emphasis.Character, emphasis)); - } + ContainerInline container = Unsafe.As(root); - emphasisMap = new CharacterMap(tempMap); + List? delimiters = null; + if (container is EmphasisDelimiterInline emphasisDelimiter) + { + delimiters = inlinesCache.Get(); + delimiters.Add(emphasisDelimiter); } - public bool PostProcess(InlineProcessor state, Inline? root, Inline? lastChild, int postInlineProcessorIndex, bool isFinalProcessing) + // Collect all EmphasisDelimiterInline by searching from the root container + var child = container.FirstChild; + while (child != null) { - if (root is null || !root.IsContainerInline) - { - return true; - } - - ContainerInline container = Unsafe.As(root); - - List? delimiters = null; - if (container is EmphasisDelimiterInline emphasisDelimiter) + // Stop the search on the delimitation child + if (child == lastChild) { - delimiters = inlinesCache.Get(); - delimiters.Add(emphasisDelimiter); + break; } - // Collect all EmphasisDelimiterInline by searching from the root container - var child = container.FirstChild; - while (child != null) + if (child.IsContainer && child is DelimiterInline delimiterInline) { - // Stop the search on the delimitation child - if (child == lastChild) + // If we have a delimiter, we search into it as we should have a tree of EmphasisDelimiterInline + if (delimiterInline is EmphasisDelimiterInline delimiter) { - break; + delimiters ??= inlinesCache.Get(); + delimiters.Add(delimiter); } - if (child.IsContainer && child is DelimiterInline delimiterInline) - { - // If we have a delimiter, we search into it as we should have a tree of EmphasisDelimiterInline - if (delimiterInline is EmphasisDelimiterInline delimiter) - { - delimiters ??= inlinesCache.Get(); - delimiters.Add(delimiter); - } - - // Follow DelimiterInline (EmphasisDelimiter, TableDelimiter...) - child = delimiterInline.FirstChild; - } - else - { - child = child.NextSibling; - } + // Follow DelimiterInline (EmphasisDelimiter, TableDelimiter...) + child = delimiterInline.FirstChild; } - - if (delimiters != null) + else { - ProcessEmphasis(state, delimiters); - inlinesCache.Release(delimiters); + child = child.NextSibling; } - return true; } - public override bool Match(InlineProcessor processor, ref StringSlice slice) + if (delimiters != null) { - // First, some definitions. - // A delimiter run is a sequence of one or more delimiter characters that is not preceded or followed by the same delimiter character - // The amount of delimiter characters in the delimiter run may exceed emphasisDesc.MaximumCount, as that is handeled in `ProcessEmphasis` + ProcessEmphasis(state, delimiters); + inlinesCache.Release(delimiters); + } + return true; + } + + public override bool Match(InlineProcessor processor, ref StringSlice slice) + { + // First, some definitions. + // A delimiter run is a sequence of one or more delimiter characters that is not preceded or followed by the same delimiter character + // The amount of delimiter characters in the delimiter run may exceed emphasisDesc.MaximumCount, as that is handeled in `ProcessEmphasis` - var delimiterChar = slice.CurrentChar; - var emphasisDesc = emphasisMap![delimiterChar]!; + var delimiterChar = slice.CurrentChar; + var emphasisDesc = emphasisMap![delimiterChar]!; - char pc = (char)0; - if (processor.Inline is HtmlEntityInline htmlEntityInline) + char pc = (char)0; + if (processor.Inline is HtmlEntityInline htmlEntityInline) + { + if (htmlEntityInline.Transcoded.Length > 0) { - if (htmlEntityInline.Transcoded.Length > 0) - { - pc = htmlEntityInline.Transcoded[htmlEntityInline.Transcoded.End]; - } + pc = htmlEntityInline.Transcoded[htmlEntityInline.Transcoded.End]; } - if (pc == 0) + } + if (pc == 0) + { + pc = slice.PeekCharExtra(-1); + if (pc == delimiterChar && slice.PeekCharExtra(-2) != '\\') { - pc = slice.PeekCharExtra(-1); - if (pc == delimiterChar && slice.PeekCharExtra(-2) != '\\') - { - // If we get here, we determined that either: - // a) there weren't enough delimiters in the delimiter run to satisfy the MinimumCount condition - // b) the previous character couldn't open/close - return false; - } + // If we get here, we determined that either: + // a) there weren't enough delimiters in the delimiter run to satisfy the MinimumCount condition + // b) the previous character couldn't open/close + return false; } - var startPosition = slice.Start; + } + var startPosition = slice.Start; - int delimiterCount = slice.CountAndSkipChar(delimiterChar); + int delimiterCount = slice.CountAndSkipChar(delimiterChar); - // If the emphasis doesn't have the minimum required character - if (delimiterCount < emphasisDesc.MinimumCount) - { - return false; - } + // If the emphasis doesn't have the minimum required character + if (delimiterCount < emphasisDesc.MinimumCount) + { + return false; + } - char c = slice.CurrentChar; + char c = slice.CurrentChar; - // The following character is actually an entity, we need to decode it - if (HtmlEntityParser.TryParse(ref slice, out string? htmlString, out int htmlLength)) - { - c = htmlString[0]; - } + // The following character is actually an entity, we need to decode it + if (HtmlEntityParser.TryParse(ref slice, out string? htmlString, out int htmlLength)) + { + c = htmlString[0]; + } - // Calculate Open-Close for current character - CharHelper.CheckOpenCloseDelimiter(pc, c, emphasisDesc.EnableWithinWord, out bool canOpen, out bool canClose); + // Calculate Open-Close for current character + CharHelper.CheckOpenCloseDelimiter(pc, c, emphasisDesc.EnableWithinWord, out bool canOpen, out bool canClose); - // We have potentially an open or close emphasis - if (canOpen || canClose) - { - var delimiterType = DelimiterType.Undefined; - if (canOpen) delimiterType |= DelimiterType.Open; - if (canClose) delimiterType |= DelimiterType.Close; + // We have potentially an open or close emphasis + if (canOpen || canClose) + { + var delimiterType = DelimiterType.Undefined; + if (canOpen) delimiterType |= DelimiterType.Open; + if (canClose) delimiterType |= DelimiterType.Close; - var delimiter = new EmphasisDelimiterInline(this, emphasisDesc) - { - DelimiterCount = delimiterCount, - Type = delimiterType, - Span = new SourceSpan(processor.GetSourcePosition(startPosition, out int line, out int column), processor.GetSourcePosition(slice.Start - 1)), - Column = column, - Line = line, - }; - - processor.Inline = delimiter; - return true; - } + var delimiter = new EmphasisDelimiterInline(this, emphasisDesc) + { + DelimiterCount = delimiterCount, + Type = delimiterType, + Span = new SourceSpan(processor.GetSourcePosition(startPosition, out int line, out int column), processor.GetSourcePosition(slice.Start - 1)), + Column = column, + Line = line, + }; - // We don't have an emphasis - return false; + processor.Inline = delimiter; + return true; } - private void ProcessEmphasis(InlineProcessor processor, List delimiters) - { - // The following method is inspired by the "An algorithm for parsing nested emphasis and links" - // at the end of the CommonMark specs. + // We don't have an emphasis + return false; + } + + private void ProcessEmphasis(InlineProcessor processor, List delimiters) + { + // The following method is inspired by the "An algorithm for parsing nested emphasis and links" + // at the end of the CommonMark specs. - // TODO: Benchmark difference between using List and LinkedList here since there could be a few Remove calls + // TODO: Benchmark difference between using List and LinkedList here since there could be a few Remove calls - // Move current_position forward in the delimiter stack (if needed) until - // we find the first potential closer with delimiter * or _. (This will be the potential closer closest to the beginning of the input – the first one in parse order.) - for (int i = 0; i < delimiters.Count; i++) + // Move current_position forward in the delimiter stack (if needed) until + // we find the first potential closer with delimiter * or _. (This will be the potential closer closest to the beginning of the input – the first one in parse order.) + for (int i = 0; i < delimiters.Count; i++) + { + var closeDelimiter = delimiters[i]; + // Skip delimiters not supported by this instance + EmphasisDescriptor? emphasisDesc = emphasisMap![closeDelimiter.DelimiterChar]; + if (emphasisDesc is null) { - var closeDelimiter = delimiters[i]; - // Skip delimiters not supported by this instance - EmphasisDescriptor? emphasisDesc = emphasisMap![closeDelimiter.DelimiterChar]; - if (emphasisDesc is null) - { - continue; - } + continue; + } - if ((closeDelimiter.Type & DelimiterType.Close) != 0 && closeDelimiter.DelimiterCount >= emphasisDesc.MinimumCount) + if ((closeDelimiter.Type & DelimiterType.Close) != 0 && closeDelimiter.DelimiterCount >= emphasisDesc.MinimumCount) + { + while (true) { - while (true) + // Now, look back in the stack (staying above stack_bottom and the openers_bottom for this delimiter type) + // for the first matching potential opener (“matching” means same delimiter). + EmphasisDelimiterInline? openDelimiter = null; + int openDelimiterIndex = -1; + for (int j = i - 1; j >= 0; j--) { - // Now, look back in the stack (staying above stack_bottom and the openers_bottom for this delimiter type) - // for the first matching potential opener (“matching” means same delimiter). - EmphasisDelimiterInline? openDelimiter = null; - int openDelimiterIndex = -1; - for (int j = i - 1; j >= 0; j--) - { - var previousOpenDelimiter = delimiters[j]; + var previousOpenDelimiter = delimiters[j]; - var isOddMatch = ((closeDelimiter.Type & DelimiterType.Open) != 0 || - (previousOpenDelimiter.Type & DelimiterType.Close) != 0) && - previousOpenDelimiter.DelimiterCount != closeDelimiter.DelimiterCount && - (previousOpenDelimiter.DelimiterCount + closeDelimiter.DelimiterCount) % 3 == 0 && - (previousOpenDelimiter.DelimiterCount % 3 != 0 || closeDelimiter.DelimiterCount % 3 != 0); + var isOddMatch = ((closeDelimiter.Type & DelimiterType.Open) != 0 || + (previousOpenDelimiter.Type & DelimiterType.Close) != 0) && + previousOpenDelimiter.DelimiterCount != closeDelimiter.DelimiterCount && + (previousOpenDelimiter.DelimiterCount + closeDelimiter.DelimiterCount) % 3 == 0 && + (previousOpenDelimiter.DelimiterCount % 3 != 0 || closeDelimiter.DelimiterCount % 3 != 0); - if (previousOpenDelimiter.DelimiterChar == closeDelimiter.DelimiterChar && - (previousOpenDelimiter.Type & DelimiterType.Open) != 0 && - previousOpenDelimiter.DelimiterCount >= emphasisDesc.MinimumCount && !isOddMatch) - { - openDelimiter = previousOpenDelimiter; - openDelimiterIndex = j; - break; - } + if (previousOpenDelimiter.DelimiterChar == closeDelimiter.DelimiterChar && + (previousOpenDelimiter.Type & DelimiterType.Open) != 0 && + previousOpenDelimiter.DelimiterCount >= emphasisDesc.MinimumCount && !isOddMatch) + { + openDelimiter = previousOpenDelimiter; + openDelimiterIndex = j; + break; } + } - if (openDelimiter != null) - { - process_delims: - Debug.Assert(openDelimiter.DelimiterCount >= emphasisDesc.MinimumCount, "Extra emphasis should have been discarded by now"); - Debug.Assert(closeDelimiter.DelimiterCount >= emphasisDesc.MinimumCount, "Extra emphasis should have been discarded by now"); - int delimiterDelta = Math.Min(Math.Min(openDelimiter.DelimiterCount, closeDelimiter.DelimiterCount), emphasisDesc.MaximumCount); + if (openDelimiter != null) + { + process_delims: + Debug.Assert(openDelimiter.DelimiterCount >= emphasisDesc.MinimumCount, "Extra emphasis should have been discarded by now"); + Debug.Assert(closeDelimiter.DelimiterCount >= emphasisDesc.MinimumCount, "Extra emphasis should have been discarded by now"); + int delimiterDelta = Math.Min(Math.Min(openDelimiter.DelimiterCount, closeDelimiter.DelimiterCount), emphasisDesc.MaximumCount); - // Insert an emph or strong emph node accordingly, after the text node corresponding to the opener. - EmphasisInline? emphasis = null; + // Insert an emph or strong emph node accordingly, after the text node corresponding to the opener. + EmphasisInline? emphasis = null; + { + if (delimiterDelta <= 2) // We can try using the legacy delegate { - if (delimiterDelta <= 2) // We can try using the legacy delegate + #pragma warning disable CS0618 // Support fields marked as obsolete + emphasis = CreateEmphasisInline?.Invoke(closeDelimiter.DelimiterChar, isStrong: delimiterDelta == 2); + #pragma warning restore CS0618 // Support fields marked as obsolete + } + if (emphasis is null) + { + // Go in backwards order to give priority to newer delegates + for (int delegateIndex = TryCreateEmphasisInlineList.Count - 1; delegateIndex >= 0; delegateIndex--) { - #pragma warning disable CS0618 // Support fields marked as obsolete - emphasis = CreateEmphasisInline?.Invoke(closeDelimiter.DelimiterChar, isStrong: delimiterDelta == 2); - #pragma warning restore CS0618 // Support fields marked as obsolete + emphasis = TryCreateEmphasisInlineList[delegateIndex].Invoke(closeDelimiter.DelimiterChar, delimiterDelta); + if (emphasis != null) break; } + if (emphasis is null) { - // Go in backwards order to give priority to newer delegates - for (int delegateIndex = TryCreateEmphasisInlineList.Count - 1; delegateIndex >= 0; delegateIndex--) + emphasis = new EmphasisInline() { - emphasis = TryCreateEmphasisInlineList[delegateIndex].Invoke(closeDelimiter.DelimiterChar, delimiterDelta); - if (emphasis != null) break; - } - - if (emphasis is null) - { - emphasis = new EmphasisInline() - { - DelimiterChar = closeDelimiter.DelimiterChar, - DelimiterCount = delimiterDelta - }; - } + DelimiterChar = closeDelimiter.DelimiterChar, + DelimiterCount = delimiterDelta + }; } } - Debug.Assert(emphasis != null); + } + Debug.Assert(emphasis != null); - // Update position for emphasis - var openDelimitercount = openDelimiter.DelimiterCount; - var closeDelimitercount = closeDelimiter.DelimiterCount; + // Update position for emphasis + var openDelimitercount = openDelimiter.DelimiterCount; + var closeDelimitercount = closeDelimiter.DelimiterCount; - emphasis!.Span.Start = openDelimiter.Span.Start; - emphasis.Line = openDelimiter.Line; - emphasis.Column = openDelimiter.Column; - emphasis.Span.End = closeDelimiter.Span.End - closeDelimitercount + delimiterDelta; + emphasis!.Span.Start = openDelimiter.Span.Start; + emphasis.Line = openDelimiter.Line; + emphasis.Column = openDelimiter.Column; + emphasis.Span.End = closeDelimiter.Span.End - closeDelimitercount + delimiterDelta; - openDelimiter.Span.Start += delimiterDelta; - openDelimiter.Column += delimiterDelta; - closeDelimiter.Span.Start += delimiterDelta; - closeDelimiter.Column += delimiterDelta; + openDelimiter.Span.Start += delimiterDelta; + openDelimiter.Column += delimiterDelta; + closeDelimiter.Span.Start += delimiterDelta; + closeDelimiter.Column += delimiterDelta; - openDelimiter.DelimiterCount -= delimiterDelta; - closeDelimiter.DelimiterCount -= delimiterDelta; + openDelimiter.DelimiterCount -= delimiterDelta; + closeDelimiter.DelimiterCount -= delimiterDelta; - var embracer = (ContainerInline)openDelimiter; + var embracer = (ContainerInline)openDelimiter; - // Copy attributes attached to delimiter to the emphasis - var attributes = closeDelimiter.TryGetAttributes(); - if (attributes != null) - { - emphasis.SetAttributes(attributes); - } + // Copy attributes attached to delimiter to the emphasis + var attributes = closeDelimiter.TryGetAttributes(); + if (attributes != null) + { + emphasis.SetAttributes(attributes); + } - // Embrace all delimiters - embracer.EmbraceChildrenBy(emphasis); + // Embrace all delimiters + embracer.EmbraceChildrenBy(emphasis); - // Remove any intermediate emphasis - for (int k = i - 1; k >= openDelimiterIndex + 1; k--) - { - var literalDelimiter = delimiters[k]; - literalDelimiter.ReplaceBy(literalDelimiter.AsLiteralInline()); - delimiters.RemoveAt(k); - i--; - } - - if (closeDelimiter.DelimiterCount == 0) - { - var newParent = openDelimiter.DelimiterCount > 0 ? emphasis : emphasis.Parent!; - closeDelimiter.MoveChildrenAfter(newParent); - closeDelimiter.Remove(); - delimiters.RemoveAt(i); - i--; + // Remove any intermediate emphasis + for (int k = i - 1; k >= openDelimiterIndex + 1; k--) + { + var literalDelimiter = delimiters[k]; + literalDelimiter.ReplaceBy(literalDelimiter.AsLiteralInline()); + delimiters.RemoveAt(k); + i--; + } - // Remove the open delimiter if it is also empty - if (openDelimiter.DelimiterCount == 0) - { - openDelimiter.MoveChildrenAfter(openDelimiter); - openDelimiter.Remove(); - delimiters.RemoveAt(openDelimiterIndex); - i--; - } - break; - } + if (closeDelimiter.DelimiterCount == 0) + { + var newParent = openDelimiter.DelimiterCount > 0 ? emphasis : emphasis.Parent!; + closeDelimiter.MoveChildrenAfter(newParent); + closeDelimiter.Remove(); + delimiters.RemoveAt(i); + i--; - // The current delimiters are matching - if (openDelimiter.DelimiterCount >= emphasisDesc.MinimumCount) - { - goto process_delims; - } - else if (openDelimiter.DelimiterCount > 0) + // Remove the open delimiter if it is also empty + if (openDelimiter.DelimiterCount == 0) { - // There are still delimiter characters left, there's just not enough of them - openDelimiter.ReplaceBy(openDelimiter.AsLiteralInline()); - delimiters.RemoveAt(openDelimiterIndex); - i--; - } - else - { - // Remove the open delimiter if it is also empty - var firstChild = openDelimiter.FirstChild!; - firstChild.Remove(); - openDelimiter.ReplaceBy(firstChild); - firstChild.IsClosed = true; - closeDelimiter.Remove(); - firstChild.InsertAfter(closeDelimiter); + openDelimiter.MoveChildrenAfter(openDelimiter); + openDelimiter.Remove(); delimiters.RemoveAt(openDelimiterIndex); i--; } + break; } - else if ((closeDelimiter.Type & DelimiterType.Open) == 0) + + // The current delimiters are matching + if (openDelimiter.DelimiterCount >= emphasisDesc.MinimumCount) { - closeDelimiter.ReplaceBy(closeDelimiter.AsLiteralInline()); - delimiters.RemoveAt(i); + goto process_delims; + } + else if (openDelimiter.DelimiterCount > 0) + { + // There are still delimiter characters left, there's just not enough of them + openDelimiter.ReplaceBy(openDelimiter.AsLiteralInline()); + delimiters.RemoveAt(openDelimiterIndex); i--; - break; } else { - break; + // Remove the open delimiter if it is also empty + var firstChild = openDelimiter.FirstChild!; + firstChild.Remove(); + openDelimiter.ReplaceBy(firstChild); + firstChild.IsClosed = true; + closeDelimiter.Remove(); + firstChild.InsertAfter(closeDelimiter); + delimiters.RemoveAt(openDelimiterIndex); + i--; } } + else if ((closeDelimiter.Type & DelimiterType.Open) == 0) + { + closeDelimiter.ReplaceBy(closeDelimiter.AsLiteralInline()); + delimiters.RemoveAt(i); + i--; + break; + } + else + { + break; + } } } + } - // Any delimiters left must be literal - for (int i = 0; i < delimiters.Count; i++) - { - var delimiter = delimiters[i]; - delimiter.ReplaceBy(delimiter.AsLiteralInline()); - } - delimiters.Clear(); + // Any delimiters left must be literal + for (int i = 0; i < delimiters.Count; i++) + { + var delimiter = delimiters[i]; + delimiter.ReplaceBy(delimiter.AsLiteralInline()); } + delimiters.Clear(); + } - public class DelimitersObjectCache : ObjectCache> + public class DelimitersObjectCache : ObjectCache> + { + protected override List NewInstance() { - protected override List NewInstance() - { - return new List(4); - } + return new List(4); + } - protected override void Reset(List instance) - { - instance.Clear(); - } + protected override void Reset(List instance) + { + instance.Clear(); } } } \ No newline at end of file diff --git a/src/Markdig/Parsers/Inlines/EscapeInlineParser.cs b/src/Markdig/Parsers/Inlines/EscapeInlineParser.cs index ffa09eb59..665bacd43 100644 --- a/src/Markdig/Parsers/Inlines/EscapeInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/EscapeInlineParser.cs @@ -5,74 +5,73 @@ using Markdig.Helpers; using Markdig.Syntax.Inlines; -namespace Markdig.Parsers.Inlines +namespace Markdig.Parsers.Inlines; + +/// +/// An inline parser for escape characters. +/// +/// +public class EscapeInlineParser : InlineParser { - /// - /// An inline parser for escape characters. - /// - /// - public class EscapeInlineParser : InlineParser + public EscapeInlineParser() + { + OpeningCharacters = new[] {'\\'}; + } + + public override bool Match(InlineProcessor processor, ref StringSlice slice) { - public EscapeInlineParser() + var startPosition = slice.Start; + // Go to escape character + var c = slice.NextChar(); + int line; + int column; + if (c.IsAsciiPunctuation()) { - OpeningCharacters = new[] {'\\'}; + processor.Inline = new LiteralInline() + { + Content = new StringSlice(slice.Text, slice.Start, slice.Start), + Span = { Start = processor.GetSourcePosition(startPosition, out line, out column) }, + Line = line, + Column = column, + IsFirstCharacterEscaped = true, + }; + processor.Inline.Span.End = processor.Inline.Span.Start + 1; + slice.SkipChar(); + return true; } - public override bool Match(InlineProcessor processor, ref StringSlice slice) + // A backslash at the end of the line is a [hard line break]: + if (c == '\n' || c == '\r') { - var startPosition = slice.Start; - // Go to escape character - var c = slice.NextChar(); - int line; - int column; - if (c.IsAsciiPunctuation()) + var newLine = c == '\n' ? NewLine.LineFeed : NewLine.CarriageReturn; + if (c == '\r' && slice.PeekChar() == '\n') { - processor.Inline = new LiteralInline() - { - Content = new StringSlice(slice.Text, slice.Start, slice.Start), - Span = { Start = processor.GetSourcePosition(startPosition, out line, out column) }, - Line = line, - Column = column, - IsFirstCharacterEscaped = true, - }; - processor.Inline.Span.End = processor.Inline.Span.Start + 1; - slice.SkipChar(); - return true; + newLine = NewLine.CarriageReturnLineFeed; } - - // A backslash at the end of the line is a [hard line break]: - if (c == '\n' || c == '\r') + var inline = new LineBreakInline() { - var newLine = c == '\n' ? NewLine.LineFeed : NewLine.CarriageReturn; - if (c == '\r' && slice.PeekChar() == '\n') - { - newLine = NewLine.CarriageReturnLineFeed; - } - var inline = new LineBreakInline() - { - IsHard = true, - IsBackslash = true, - Span = { Start = processor.GetSourcePosition(startPosition, out line, out column) }, - Line = line, - Column = column, - }; - processor.Inline = inline; + IsHard = true, + IsBackslash = true, + Span = { Start = processor.GetSourcePosition(startPosition, out line, out column) }, + Line = line, + Column = column, + }; + processor.Inline = inline; - if (processor.TrackTrivia) - { - inline.NewLine = newLine; - } - - inline.Span.End = inline.Span.Start + 1; - slice.SkipChar(); // Skip \n or \r alone - if (newLine == NewLine.CarriageReturnLineFeed) - { - slice.SkipChar(); // Skip \r\n - } - return true; + if (processor.TrackTrivia) + { + inline.NewLine = newLine; } - - return false; + + inline.Span.End = inline.Span.Start + 1; + slice.SkipChar(); // Skip \n or \r alone + if (newLine == NewLine.CarriageReturnLineFeed) + { + slice.SkipChar(); // Skip \r\n + } + return true; } + + return false; } } \ No newline at end of file diff --git a/src/Markdig/Parsers/Inlines/HtmlEntityParser.cs b/src/Markdig/Parsers/Inlines/HtmlEntityParser.cs index 8cfa1a124..a52f4a358 100644 --- a/src/Markdig/Parsers/Inlines/HtmlEntityParser.cs +++ b/src/Markdig/Parsers/Inlines/HtmlEntityParser.cs @@ -2,76 +2,73 @@ // 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.CodeAnalysis; using Markdig.Helpers; using Markdig.Syntax; using Markdig.Syntax.Inlines; -namespace Markdig.Parsers.Inlines +namespace Markdig.Parsers.Inlines; + +/// +/// An inline parser for HTML entities. +/// +/// +public class HtmlEntityParser : InlineParser { /// - /// An inline parser for HTML entities. + /// Initializes a new instance of the class. /// - /// - public class HtmlEntityParser : InlineParser + public HtmlEntityParser() + { + OpeningCharacters = new[] {'&'}; + } + + public static bool TryParse(ref StringSlice slice, [NotNullWhen(true)] out string? literal, out int match) { - /// - /// Initializes a new instance of the class. - /// - public HtmlEntityParser() + literal = null; + match = HtmlHelper.ScanEntity(slice, out int entityValue, out int entityNameStart, out int entityNameLength); + if (match == 0) { - OpeningCharacters = new[] {'&'}; + return false; } - - public static bool TryParse(ref StringSlice slice, [NotNullWhen(true)] out string? literal, out int match) + if (entityNameLength > 0) { - literal = null; - match = HtmlHelper.ScanEntity(slice, out int entityValue, out int entityNameStart, out int entityNameLength); - if (match == 0) - { - return false; - } - - if (entityNameLength > 0) - { - literal = EntityHelper.DecodeEntity(slice.Text.AsSpan(entityNameStart, entityNameLength)); - } - else if (entityValue >= 0) - { - literal = EntityHelper.DecodeEntity(entityValue); - } - return literal != null; + literal = EntityHelper.DecodeEntity(slice.Text.AsSpan(entityNameStart, entityNameLength)); } + else if (entityValue >= 0) + { + literal = EntityHelper.DecodeEntity(entityValue); + } + return literal != null; + } - public override bool Match(InlineProcessor processor, ref StringSlice slice) + public override bool Match(InlineProcessor processor, ref StringSlice slice) + { + if (!TryParse(ref slice, out string? literal, out int match)) { - if (!TryParse(ref slice, out string? literal, out int match)) - { - return false; - } + return false; + } - var startPosition = slice.Start; + var startPosition = slice.Start; - if (literal != null) + if (literal != null) + { + var matched = slice; + matched.End = slice.Start + match - 1; + processor.Inline = new HtmlEntityInline() { - var matched = slice; - matched.End = slice.Start + match - 1; - processor.Inline = new HtmlEntityInline() - { - Original = matched, - Transcoded = new StringSlice(literal), - Span = new SourceSpan(processor.GetSourcePosition(startPosition, out int line, out int column), processor.GetSourcePosition(matched.End)), - Line = line, - Column = column - }; - slice.Start = slice.Start + match; - return true; - } - - return false; + Original = matched, + Transcoded = new StringSlice(literal), + Span = new SourceSpan(processor.GetSourcePosition(startPosition, out int line, out int column), processor.GetSourcePosition(matched.End)), + Line = line, + Column = column + }; + slice.Start = slice.Start + match; + return true; } + + return false; } } \ No newline at end of file diff --git a/src/Markdig/Parsers/Inlines/LineBreakInlineParser.cs b/src/Markdig/Parsers/Inlines/LineBreakInlineParser.cs index 894b2882d..6bfdd9ffe 100644 --- a/src/Markdig/Parsers/Inlines/LineBreakInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/LineBreakInlineParser.cs @@ -3,79 +3,77 @@ // See the license.txt file in the project root for more information. using Markdig.Helpers; -using Markdig.Syntax; using Markdig.Syntax.Inlines; -namespace Markdig.Parsers.Inlines +namespace Markdig.Parsers.Inlines; + +/// +/// An inline parser for . +/// +/// +public class LineBreakInlineParser : InlineParser { /// - /// An inline parser for . + /// Initializes a new instance of the class. /// - /// - public class LineBreakInlineParser : InlineParser + public LineBreakInlineParser() { - /// - /// Initializes a new instance of the class. - /// - public LineBreakInlineParser() - { - OpeningCharacters = new[] { '\n', '\r' }; - } + OpeningCharacters = new[] { '\n', '\r' }; + } - /// - /// Gets or sets a value indicating whether to interpret softline breaks as hardline breaks. Default is false - /// - public bool EnableSoftAsHard { get; set; } + /// + /// Gets or sets a value indicating whether to interpret softline breaks as hardline breaks. Default is false + /// + public bool EnableSoftAsHard { get; set; } - public override bool Match(InlineProcessor processor, ref StringSlice slice) + 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!.IsParagraphBlock) { - // 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!.IsParagraphBlock) - { - return false; - } + return false; + } - var startPosition = slice.Start; - var hasDoubleSpacesBefore = slice.PeekCharExtra(-1).IsSpace() && slice.PeekCharExtra(-2).IsSpace(); - var newLine = NewLine.LineFeed; - if (processor.TrackTrivia) + var startPosition = slice.Start; + var hasDoubleSpacesBefore = slice.PeekCharExtra(-1).IsSpace() && slice.PeekCharExtra(-2).IsSpace(); + var newLine = NewLine.LineFeed; + if (processor.TrackTrivia) + { + if (slice.CurrentChar == '\r') { - if (slice.CurrentChar == '\r') + if (slice.PeekChar() == '\n') { - if (slice.PeekChar() == '\n') - { - newLine = NewLine.CarriageReturnLineFeed; - slice.SkipChar(); // Skip \n - } - else - { - newLine = NewLine.CarriageReturn; - } + newLine = NewLine.CarriageReturnLineFeed; + slice.SkipChar(); // Skip \n } else { - newLine = NewLine.LineFeed; + newLine = NewLine.CarriageReturn; } } else { - if (slice.CurrentChar == '\r' && slice.PeekChar() == '\n') - { - slice.SkipChar(); // Skip \n - } + newLine = NewLine.LineFeed; } - slice.SkipChar(); // Skip \r or \n - - processor.Inline = new LineBreakInline + } + else + { + if (slice.CurrentChar == '\r' && slice.PeekChar() == '\n') { - Span = { Start = processor.GetSourcePosition(startPosition, out int line, out int column) }, - IsHard = EnableSoftAsHard || (slice.Start != 0 && hasDoubleSpacesBefore), - Line = line, - Column = column, - NewLine = newLine - }; - processor.Inline.Span.End = processor.Inline.Span.Start + (newLine == NewLine.CarriageReturnLineFeed ? 1 : 0); - return true; + slice.SkipChar(); // Skip \n + } } + slice.SkipChar(); // Skip \r or \n + + processor.Inline = new LineBreakInline + { + Span = { Start = processor.GetSourcePosition(startPosition, out int line, out int column) }, + IsHard = EnableSoftAsHard || (slice.Start != 0 && hasDoubleSpacesBefore), + Line = line, + Column = column, + NewLine = newLine + }; + processor.Inline.Span.End = processor.Inline.Span.Start + (newLine == NewLine.CarriageReturnLineFeed ? 1 : 0); + return true; } } \ No newline at end of file diff --git a/src/Markdig/Parsers/Inlines/LinkInlineParser.cs b/src/Markdig/Parsers/Inlines/LinkInlineParser.cs index 853bd07ca..211f552f8 100644 --- a/src/Markdig/Parsers/Inlines/LinkInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/LinkInlineParser.cs @@ -5,422 +5,420 @@ using Markdig.Helpers; using Markdig.Syntax; using Markdig.Syntax.Inlines; -using System.Diagnostics.CodeAnalysis; -namespace Markdig.Parsers.Inlines +namespace Markdig.Parsers.Inlines; + +/// +/// An inline parser for . +/// +/// +public class LinkInlineParser : InlineParser { /// - /// An inline parser for . + /// Initializes a new instance of the class. /// - /// - public class LinkInlineParser : InlineParser + public LinkInlineParser() { - /// - /// Initializes a new instance of the class. - /// - public LinkInlineParser() - { - OpeningCharacters = new[] {'[', ']', '!'}; - } + OpeningCharacters = new[] {'[', ']', '!'}; + } - public override bool Match(InlineProcessor processor, ref StringSlice slice) - { - // The following methods are inspired by the "An algorithm for parsing nested emphasis and links" - // at the end of the CommonMark specs. + public override bool Match(InlineProcessor processor, ref StringSlice slice) + { + // The following methods are inspired by the "An algorithm for parsing nested emphasis and links" + // at the end of the CommonMark specs. - var c = slice.CurrentChar; + var c = slice.CurrentChar; - var startPosition = processor.GetSourcePosition(slice.Start, out int line, out int column); + var startPosition = processor.GetSourcePosition(slice.Start, out int line, out int column); - bool isImage = false; - if (c == '!') + bool isImage = false; + if (c == '!') + { + isImage = true; + c = slice.NextChar(); + if (c != '[') { - isImage = true; - c = slice.NextChar(); - if (c != '[') - { - return false; - } + return false; } - string? label; - SourceSpan labelWithTriviaSpan = SourceSpan.Empty; - switch (c) - { - case '[': - // If this is not an image, we may have a reference link shortcut - // so we try to resolve it here - var saved = slice; - - SourceSpan labelSpan; - // If the label is followed by either a ( or a [, this is not a shortcut - if (processor.TrackTrivia) + } + string? label; + SourceSpan labelWithTriviaSpan = SourceSpan.Empty; + switch (c) + { + case '[': + // If this is not an image, we may have a reference link shortcut + // so we try to resolve it here + var saved = slice; + + SourceSpan labelSpan; + // If the label is followed by either a ( or a [, this is not a shortcut + if (processor.TrackTrivia) + { + if (LinkHelper.TryParseLabelTrivia(ref slice, out label, out labelSpan)) { - if (LinkHelper.TryParseLabelTrivia(ref slice, out label, out labelSpan)) + labelWithTriviaSpan.Start = labelSpan.Start; // skip opening [ + labelWithTriviaSpan.End = labelSpan.End; // skip closing ] + if (!processor.Document.ContainsLinkReferenceDefinition(label)) { - labelWithTriviaSpan.Start = labelSpan.Start; // skip opening [ - labelWithTriviaSpan.End = labelSpan.End; // skip closing ] - if (!processor.Document.ContainsLinkReferenceDefinition(label)) - { - label = null; - } + label = null; } } - else + } + else + { + if (LinkHelper.TryParseLabel(ref slice, out label, out labelSpan)) { - if (LinkHelper.TryParseLabel(ref slice, out label, out labelSpan)) + if (!processor.Document.ContainsLinkReferenceDefinition(label)) { - if (!processor.Document.ContainsLinkReferenceDefinition(label)) - { - label = null; - } + label = null; } } - slice = saved; + } + slice = saved; - // Else we insert a LinkDelimiter - slice.SkipChar(); - var linkDelimiter = new LinkDelimiterInline(this) - { - Type = DelimiterType.Open, - Label = label, - LabelSpan = processor.GetSourcePositionFromLocalSpan(labelSpan), - IsImage = isImage, - Span = new SourceSpan(startPosition, processor.GetSourcePosition(slice.Start - 1)), - Line = line, - Column = column - }; + // Else we insert a LinkDelimiter + slice.SkipChar(); + var linkDelimiter = new LinkDelimiterInline(this) + { + Type = DelimiterType.Open, + Label = label, + 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); - } + if (processor.TrackTrivia) + { + linkDelimiter.LabelWithTrivia = new StringSlice(slice.Text, labelWithTriviaSpan.Start, labelWithTriviaSpan.End); + } - processor.Inline = linkDelimiter; - return true; + processor.Inline = linkDelimiter; + return true; - case ']': - slice.SkipChar(); - if (processor.Inline != null) + case ']': + slice.SkipChar(); + if (processor.Inline != null) + { + if (TryProcessLinkOrImage(processor, ref slice)) { - if (TryProcessLinkOrImage(processor, ref slice)) - { - return true; - } + return true; } + } - // If we don’t find one, we return a literal slice node ]. - // (Done after by the LiteralInline parser) - return false; - } + // If we don’t find one, we return a literal slice node ]. + // (Done after by the LiteralInline parser) + return false; + } + + // We don't have an emphasis + return false; + } - // We don't have an emphasis + private bool ProcessLinkReference( + InlineProcessor state, + StringSlice text, + string label, + SourceSpan labelWithriviaSpan, + bool isShortcut, + SourceSpan labelSpan, + LinkDelimiterInline parent, + int endPosition, + LocalLabel localLabel) + { + if (!state.Document.TryGetLinkReferenceDefinition(label, out LinkReferenceDefinition? linkRef)) + { return false; } - private bool ProcessLinkReference( - InlineProcessor state, - StringSlice text, - string label, - SourceSpan labelWithriviaSpan, - bool isShortcut, - SourceSpan labelSpan, - LinkDelimiterInline parent, - int endPosition, - LocalLabel localLabel) + Inline? link = null; + // Try to use a callback directly defined on the LinkReferenceDefinition + if (linkRef.CreateLinkInline != null) + { + link = linkRef.CreateLinkInline(state, linkRef, parent.FirstChild); + } + + // Create a default link if the callback was not found + if (link is null) { - if (!state.Document.TryGetLinkReferenceDefinition(label, out LinkReferenceDefinition? linkRef)) + // Inline Link + var linkInline = new LinkInline() { - return false; - } + Url = HtmlHelper.Unescape(linkRef.Url), + Title = HtmlHelper.Unescape(linkRef.Title), + Label = label, + LabelSpan = labelSpan, + UrlSpan = linkRef.UrlSpan, + IsImage = parent.IsImage, + IsShortcut = isShortcut, + Reference = linkRef, + Span = new SourceSpan(parent.Span.Start, endPosition), + Line = parent.Line, + Column = parent.Column, + }; - Inline? link = null; - // Try to use a callback directly defined on the LinkReferenceDefinition - if (linkRef.CreateLinkInline != null) + if (state.TrackTrivia) { - link = linkRef.CreateLinkInline(state, linkRef, parent.FirstChild); + linkInline.LabelWithTrivia = new StringSlice(text.Text, labelWithriviaSpan.Start, labelWithriviaSpan.End); + linkInline.LinkRefDefLabel = linkRef.Label; + linkInline.LinkRefDefLabelWithTrivia = linkRef.LabelWithTrivia; + linkInline.LocalLabel = localLabel; } - // Create a default link if the callback was not found - if (link is null) + link = linkInline; + } + + if (link is ContainerInline containerLink) + { + var child = parent.FirstChild; + if (child is null) { - // Inline Link - var linkInline = new LinkInline() + child = new LiteralInline() { - Url = HtmlHelper.Unescape(linkRef.Url), - Title = HtmlHelper.Unescape(linkRef.Title), - Label = label, - LabelSpan = labelSpan, - UrlSpan = linkRef.UrlSpan, - IsImage = parent.IsImage, - IsShortcut = isShortcut, - Reference = linkRef, - Span = new SourceSpan(parent.Span.Start, endPosition), + Content = StringSlice.Empty, + IsClosed = true, + // Not exact but we leave it like this + Span = parent.Span, 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; + containerLink.AppendChild(child); } - - if (link is ContainerInline containerLink) + else { - var child = parent.FirstChild; - if (child is null) + // Insert all child into the link + while (child != null) { - child = new LiteralInline() - { - Content = StringSlice.Empty, - IsClosed = true, - // Not exact but we leave it like this - Span = parent.Span, - Line = parent.Line, - Column = parent.Column, - }; + var next = child.NextSibling; + child.Remove(); containerLink.AppendChild(child); - } - else - { - // Insert all child into the link - while (child != null) - { - var next = child.NextSibling; - child.Remove(); - containerLink.AppendChild(child); - child = next; - } + child = next; } } + } - link.IsClosed = true; + link.IsClosed = true; - // Process emphasis delimiters - state.PostProcessInlines(0, link, null, false); + // Process emphasis delimiters + state.PostProcessInlines(0, link, null, false); - state.Inline = link; + state.Inline = link; - return true; + return true; + } + + private bool TryProcessLinkOrImage(InlineProcessor inlineState, ref StringSlice text) + { + LinkDelimiterInline? openParent = inlineState.Inline!.FirstParentOfType(); + + if (openParent is null) + { + return false; + } + + // If we do find one, but it’s not active, + // we remove the inactive delimiter from the stack, + // and return a literal text node ]. + if (!openParent.IsActive) + { + inlineState.Inline = new LiteralInline() + { + Content = new StringSlice("["), + Span = openParent.Span, + Line = openParent.Line, + Column = openParent.Column, + }; + openParent.ReplaceBy(inlineState.Inline); + return false; } - private bool TryProcessLinkOrImage(InlineProcessor inlineState, ref StringSlice text) + // If we find one and it’s active, + // then we parse ahead to see if we have + // an inline link/image, reference link/image, + // compact reference link/image, + // or shortcut reference link/image + var parentDelimiter = openParent.Parent; + var savedText = text; + + if (text.CurrentChar == '(') { - LinkDelimiterInline? openParent = inlineState.Inline!.FirstParentOfType(); + LinkInline? link = null; - if (openParent is null) + if (inlineState.TrackTrivia) { - return false; + link = TryParseInlineLinkTrivia(ref text, inlineState, openParent); } - - // If we do find one, but it’s not active, - // we remove the inactive delimiter from the stack, - // and return a literal text node ]. - if (!openParent.IsActive) + else { - inlineState.Inline = new LiteralInline() + if (LinkHelper.TryParseInlineLink(ref text, out string? url, out string? title, out SourceSpan linkSpan, out SourceSpan titleSpan)) { - Content = new StringSlice("["), - Span = openParent.Span, - Line = openParent.Line, - Column = openParent.Column, - }; - openParent.ReplaceBy(inlineState.Inline); - return false; + // Inline Link + link = new LinkInline() + { + Url = HtmlHelper.Unescape(url), + Title = HtmlHelper.Unescape(title), + 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, + }; + } } - // If we find one and it’s active, - // then we parse ahead to see if we have - // an inline link/image, reference link/image, - // compact reference link/image, - // or shortcut reference link/image - var parentDelimiter = openParent.Parent; - var savedText = text; - - if (text.CurrentChar == '(') + if (link is not null) { - LinkInline? link = null; + openParent.ReplaceBy(link); + // Notifies processor as we are creating an inline locally + inlineState.Inline = link; - if (inlineState.TrackTrivia) - { - 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 - link = new LinkInline() - { - Url = HtmlHelper.Unescape(url), - Title = HtmlHelper.Unescape(title), - 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, - }; - } - } + // Process emphasis delimiters + inlineState.PostProcessInlines(0, link, null, false); - if (link is not null) + // 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) { - openParent.ReplaceBy(link); - // Notifies processor as we are creating an inline locally - inlineState.Inline = link; + MarkParentAsInactive(parentDelimiter); + } - // Process emphasis delimiters - inlineState.PostProcessInlines(0, link, null, false); + link.IsClosed = true; - // 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); - } + return true; + } - link.IsClosed = true; + text = savedText; + } - return true; - } + var labelSpan = SourceSpan.Empty; + string? label = null; + bool isLabelSpanLocal = true; - text = savedText; + bool isShortcut = false; + LocalLabel localLabel = LocalLabel.Local; + // Handle Collapsed links + if (text.CurrentChar == '[') + { + if (text.PeekChar() == ']') + { + label = openParent.Label; + labelSpan = openParent.LabelSpan; + isLabelSpanLocal = false; + localLabel = LocalLabel.Empty; + text.SkipChar(); // Skip [ + text.SkipChar(); // Skip ] } + } + else + { + localLabel = LocalLabel.None; + label = openParent.Label; + isShortcut = true; + } - var labelSpan = SourceSpan.Empty; - string? label = null; - bool isLabelSpanLocal = true; + if (label != null || LinkHelper.TryParseLabelTrivia(ref text, true, out label, out labelSpan)) + { + SourceSpan labelWithTrivia = new SourceSpan(labelSpan.Start, labelSpan.End); + if (isLabelSpanLocal) + { + labelSpan = inlineState.GetSourcePositionFromLocalSpan(labelSpan); + } - bool isShortcut = false; - LocalLabel localLabel = LocalLabel.Local; - // Handle Collapsed links - if (text.CurrentChar == '[') + if (ProcessLinkReference(inlineState, text, label!, labelWithTrivia, isShortcut, labelSpan, openParent, inlineState.GetSourcePosition(text.Start - 1), localLabel)) { - if (text.PeekChar() == ']') + // Remove the open parent + openParent.Remove(); + if (!openParent.IsImage) { - label = openParent.Label; - labelSpan = openParent.LabelSpan; - isLabelSpanLocal = false; - localLabel = LocalLabel.Empty; - text.SkipChar(); // Skip [ - text.SkipChar(); // Skip ] + MarkParentAsInactive(parentDelimiter); } + return true; } - else + else if (text.CurrentChar != ']' && text.CurrentChar != '[') { - localLabel = LocalLabel.None; - label = openParent.Label; - isShortcut = true; + return false; } + } - if (label != null || LinkHelper.TryParseLabelTrivia(ref text, true, out label, out labelSpan)) - { - SourceSpan labelWithTrivia = new SourceSpan(labelSpan.Start, labelSpan.End); - if (isLabelSpanLocal) - { - labelSpan = inlineState.GetSourcePositionFromLocalSpan(labelSpan); - } + // We have a nested [ ] + // firstParent.Remove(); + // The opening [ will be transformed to a literal followed by all the children of the [ - if (ProcessLinkReference(inlineState, text, label!, labelWithTrivia, isShortcut, labelSpan, openParent, inlineState.GetSourcePosition(text.Start - 1), localLabel)) - { - // Remove the open parent - openParent.Remove(); - if (!openParent.IsImage) - { - MarkParentAsInactive(parentDelimiter); - } - return true; - } - else if (text.CurrentChar != ']' && text.CurrentChar != '[') - { - return false; - } - } + var literal = new LiteralInline() + { + Span = openParent.Span, + Content = new StringSlice(openParent.IsImage ? "![" : "[") + }; - // We have a nested [ ] - // firstParent.Remove(); - // The opening [ will be transformed to a literal followed by all the children of the [ + inlineState.Inline = openParent.ReplaceBy(literal); + return false; - var literal = new LiteralInline() + 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)) { - Span = openParent.Span, - Content = new StringSlice(openParent.IsImage ? "![" : "[") - }; - - inlineState.Inline = openParent.ReplaceBy(literal); - return false; + 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); - 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)) + return new LinkInline() { - 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; + 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 static void MarkParentAsInactive(Inline? inline) + private static void MarkParentAsInactive(Inline? inline) + { + while (inline != null) { - while (inline != null) + if (inline is LinkDelimiterInline linkInline) { - if (inline is LinkDelimiterInline linkInline) + if (linkInline.IsImage) { - if (linkInline.IsImage) - { - break; - } - - linkInline.IsActive = false; + break; } - inline = inline.Parent; + linkInline.IsActive = false; } + + inline = inline.Parent; } } } \ No newline at end of file diff --git a/src/Markdig/Parsers/Inlines/LiteralInlineParser.cs b/src/Markdig/Parsers/Inlines/LiteralInlineParser.cs index a9231a72d..225b537e8 100644 --- a/src/Markdig/Parsers/Inlines/LiteralInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/LiteralInlineParser.cs @@ -6,102 +6,101 @@ using Markdig.Syntax; using Markdig.Syntax.Inlines; -namespace Markdig.Parsers.Inlines +namespace Markdig.Parsers.Inlines; + +/// +/// An inline parser for parsing . +/// +/// +public sealed class LiteralInlineParser : InlineParser { + public delegate void PostMatchDelegate(InlineProcessor processor, ref StringSlice slice); + /// - /// An inline parser for parsing . + /// We don't expect the LiteralInlineParser to be instantiated a end-user, as it is part + /// of the default parser pipeline (and should always be the last), working as a literal character + /// collector. /// - /// - public sealed class LiteralInlineParser : InlineParser + public LiteralInlineParser() { - public delegate void PostMatchDelegate(InlineProcessor processor, ref StringSlice slice); - - /// - /// We don't expect the LiteralInlineParser to be instantiated a end-user, as it is part - /// of the default parser pipeline (and should always be the last), working as a literal character - /// collector. - /// - public LiteralInlineParser() - { - } + } - /// - /// Gets or sets the post match delegate called after the inline has been processed. - /// - public PostMatchDelegate? PostMatch { get; set; } + /// + /// Gets or sets the post match delegate called after the inline has been processed. + /// + public PostMatchDelegate? PostMatch { get; set; } - public override bool Match(InlineProcessor processor, ref StringSlice slice) - { - var text = slice.Text; + public override bool Match(InlineProcessor processor, ref StringSlice slice) + { + var text = slice.Text; - var startPosition = processor.GetSourcePosition(slice.Start, out int line, out int column); + var startPosition = processor.GetSourcePosition(slice.Start, out int line, out int column); - // Slightly faster to perform our own search for opening characters - var nextStart = processor.Parsers.IndexOfOpeningCharacter(text, slice.Start + 1, slice.End); - //var nextStart = str.IndexOfAny(processor.SpecialCharacters, slice.Start + 1, slice.Length - 1); - int length; + // Slightly faster to perform our own search for opening characters + var nextStart = processor.Parsers.IndexOfOpeningCharacter(text, slice.Start + 1, slice.End); + //var nextStart = str.IndexOfAny(processor.SpecialCharacters, slice.Start + 1, slice.Length - 1); + int length; - if (nextStart < 0) - { - nextStart = slice.End + 1; - length = nextStart - slice.Start; - } - else + if (nextStart < 0) + { + nextStart = slice.End + 1; + length = nextStart - slice.Start; + } + else + { + // Remove line endings if the next char is a new line + length = nextStart - slice.Start; + if (!processor.TrackTrivia) { - // Remove line endings if the next char is a new line - length = nextStart - slice.Start; - if (!processor.TrackTrivia) + var nextText = text[nextStart]; + if (nextText == '\n' || nextText == '\r') { - var nextText = text[nextStart]; - if (nextText == '\n' || nextText == '\r') + int end = nextStart - 1; + while (length > 0 && text[end].IsSpace()) { - int end = nextStart - 1; - while (length > 0 && text[end].IsSpace()) - { - length--; - end--; - } + length--; + end--; } } } + } - // The LiteralInlineParser is always matching (at least an empty string) - var endPosition = slice.Start + length - 1; + // The LiteralInlineParser is always matching (at least an empty string) + var endPosition = slice.Start + length - 1; - if (processor.Inline is { } previousInline - && !previousInline.IsContainer - && processor.Inline is LiteralInline previousLiteral - && ReferenceEquals(previousLiteral.Content.Text, slice.Text) - && previousLiteral.Content.End + 1 == slice.Start) - { - previousLiteral.Content.End = endPosition; - previousLiteral.Span.End = processor.GetSourcePosition(endPosition); - } - else + if (processor.Inline is { } previousInline + && !previousInline.IsContainer + && processor.Inline is LiteralInline previousLiteral + && ReferenceEquals(previousLiteral.Content.Text, slice.Text) + && previousLiteral.Content.End + 1 == slice.Start) + { + previousLiteral.Content.End = endPosition; + previousLiteral.Span.End = processor.GetSourcePosition(endPosition); + } + else + { + // Create a new LiteralInline only if it is not empty + var newSlice = length > 0 ? new StringSlice(slice.Text, slice.Start, endPosition) : StringSlice.Empty; + if (!newSlice.IsEmpty) { - // Create a new LiteralInline only if it is not empty - var newSlice = length > 0 ? new StringSlice(slice.Text, slice.Start, endPosition) : StringSlice.Empty; - if (!newSlice.IsEmpty) + processor.Inline = new LiteralInline { - processor.Inline = new LiteralInline - { - Content = length > 0 ? newSlice : StringSlice.Empty, - Span = new SourceSpan(startPosition, processor.GetSourcePosition(endPosition)), - Line = line, - Column = column, - }; - } + Content = length > 0 ? newSlice : StringSlice.Empty, + Span = new SourceSpan(startPosition, processor.GetSourcePosition(endPosition)), + Line = line, + Column = column, + }; } + } - slice.Start = nextStart; + slice.Start = nextStart; - // Call only PostMatch if necessary - if (processor.Inline is LiteralInline) - { - PostMatch?.Invoke(processor, ref slice); - } - - return true; + // Call only PostMatch if necessary + if (processor.Inline is LiteralInline) + { + PostMatch?.Invoke(processor, ref slice); } + + return true; } } \ No newline at end of file diff --git a/src/Markdig/Parsers/ListBlockParser.cs b/src/Markdig/Parsers/ListBlockParser.cs index 45aad6f27..a17a48c7f 100644 --- a/src/Markdig/Parsers/ListBlockParser.cs +++ b/src/Markdig/Parsers/ListBlockParser.cs @@ -2,388 +2,386 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using System.Collections.Generic; using Markdig.Helpers; using Markdig.Syntax; -namespace Markdig.Parsers +namespace Markdig.Parsers; + +/// +/// A parser for a list block and list item block. +/// +/// +public class ListBlockParser : BlockParser { + private CharacterMap? mapItemParsers; + /// - /// A parser for a list block and list item block. + /// Initializes a new instance of the class. /// - /// - public class ListBlockParser : BlockParser + public ListBlockParser() { - private CharacterMap? mapItemParsers; - - /// - /// Initializes a new instance of the class. - /// - public ListBlockParser() + ItemParsers = new OrderedList() { - ItemParsers = new OrderedList() - { - new UnorderedListItemParser(), - new NumberedListItemParser() - }; - } + new UnorderedListItemParser(), + new NumberedListItemParser() + }; + } + + /// + /// Gets the parsers for items. + /// + public OrderedList ItemParsers { get; } - /// - /// Gets the parsers for items. - /// - public OrderedList ItemParsers { get; } + public override void Initialize() + { + var tempMap = new Dictionary(); - public override void Initialize() + foreach (var itemParser in ItemParsers) { - var tempMap = new Dictionary(); - - foreach (var itemParser in ItemParsers) + if (itemParser.OpeningCharacters is null) { - if (itemParser.OpeningCharacters is null) - { - ThrowHelper.InvalidOperationException($"The list item parser of type [{itemParser.GetType()}] cannot have OpeningCharacters to null. It must define a list of valid opening characters"); - } + ThrowHelper.InvalidOperationException($"The list item parser of type [{itemParser.GetType()}] cannot have OpeningCharacters to null. It must define a list of valid opening characters"); + } - foreach (var openingCharacter in itemParser.OpeningCharacters) + foreach (var openingCharacter in itemParser.OpeningCharacters) + { + if (tempMap.ContainsKey(openingCharacter)) { - if (tempMap.ContainsKey(openingCharacter)) - { - ThrowHelper.InvalidOperationException( - $"A list item parser with the same opening character `{openingCharacter}` is already registered"); - } - tempMap.Add(openingCharacter, itemParser); + ThrowHelper.InvalidOperationException( + $"A list item parser with the same opening character `{openingCharacter}` is already registered"); } + tempMap.Add(openingCharacter, itemParser); } - mapItemParsers = new CharacterMap(tempMap); } + mapItemParsers = new CharacterMap(tempMap); + } - public override BlockState TryOpen(BlockProcessor processor) + public override BlockState TryOpen(BlockProcessor processor) + { + // When both a thematic break and a list item are possible + // interpretations of a line, the thematic break takes precedence + var thematicParser = ThematicBreakParser.Default; + if (thematicParser.HasOpeningCharacter(processor.CurrentChar)) { - // When both a thematic break and a list item are possible - // interpretations of a line, the thematic break takes precedence - var thematicParser = ThematicBreakParser.Default; - if (thematicParser.HasOpeningCharacter(processor.CurrentChar)) + var result = thematicParser.TryOpen(processor); + if (result.IsBreak()) { - var result = thematicParser.TryOpen(processor); - if (result.IsBreak()) - { - return result; - } + return result; } + } - return TryParseListItem(processor, null); + return TryParseListItem(processor, null); + } + + public override BlockState TryContinue(BlockProcessor processor, Block block) + { + if (block is ListBlock && processor.NextContinue is ListItemBlock) + { + // We try to match only on item block if the ListBlock + return BlockState.Skip; } - public override BlockState TryContinue(BlockProcessor processor, Block block) + // When both a thematic break and a list item are possible + // interpretations of a line, the thematic break takes precedence + BlockState result; + var thematicParser = ThematicBreakParser.Default; + if (processor.LastBlock is not FencedCodeBlock && thematicParser.HasOpeningCharacter(processor.CurrentChar)) { - if (block is ListBlock && processor.NextContinue is ListItemBlock) + result = thematicParser.TryOpen(processor); + if (result.IsBreak()) { - // We try to match only on item block if the ListBlock - return BlockState.Skip; - } + // 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(); - // When both a thematic break and a list item are possible - // interpretations of a line, the thematic break takes precedence - BlockState result; - var thematicParser = ThematicBreakParser.Default; - if (processor.LastBlock is not FencedCodeBlock && thematicParser.HasOpeningCharacter(processor.CurrentChar)) - { - result = thematicParser.TryOpen(processor); - if (result.IsBreak()) + if (processor.TrackTrivia) { - // 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(); - - if (processor.TrackTrivia) - { - processor.LinesBefore = thematicBreak.LinesBefore; - } - - return BlockState.None; + processor.LinesBefore = thematicBreak.LinesBefore; } - } - // 5.2 List items - // TODO: Check with specs, it is not clear that list marker or bullet marker must be followed by at least 1 space - - // If we have already a ListItemBlock, we are going to try to append to it - result = BlockState.None; - if (block is ListItemBlock listItem) - { - result = TryContinueListItem(processor, listItem); + return BlockState.None; } + } - if (result == BlockState.None) - { - result = TryParseListItem(processor, block); - } + // 5.2 List items + // TODO: Check with specs, it is not clear that list marker or bullet marker must be followed by at least 1 space - return result; + // If we have already a ListItemBlock, we are going to try to append to it + result = BlockState.None; + if (block is ListItemBlock listItem) + { + result = TryContinueListItem(processor, listItem); } - private BlockState TryContinueListItem(BlockProcessor state, ListItemBlock listItem) + if (result == BlockState.None) { - var list = (ListBlock)listItem.Parent!; + result = TryParseListItem(processor, block); + } + + return result; + } + + private BlockState TryContinueListItem(BlockProcessor state, ListItemBlock listItem) + { + var list = (ListBlock)listItem.Parent!; - // Allow all blanks lines if the last block is a fenced code block - // Allow 1 blank line inside a list - // If > 1 blank line, terminate this list - if (state.IsBlankLine) + // Allow all blanks lines if the last block is a fenced code block + // Allow 1 blank line inside a list + // If > 1 blank line, terminate this list + if (state.IsBlankLine) + { + if (state.CurrentBlock != null && state.CurrentBlock.IsBreakable) { - if (state.CurrentBlock != null && state.CurrentBlock.IsBreakable) + if (!(state.NextContinue is ListBlock)) { - if (!(state.NextContinue is ListBlock)) + list.CountAllBlankLines++; + if (!state.TrackTrivia) { - list.CountAllBlankLines++; - if (!state.TrackTrivia) - { - listItem.Add(new BlankLineBlock()); - } + listItem.Add(new BlankLineBlock()); } - list.CountBlankLinesReset++; - } - - if (list.CountBlankLinesReset == 1 && listItem.ColumnWidth < 0) - { - state.Close(listItem); - - // Leave the list open - list.IsOpen = true; - return BlockState.Continue; } + list.CountBlankLinesReset++; + } - // Update list-item source end position - listItem.UpdateSpanEnd(state.Line.End); + if (list.CountBlankLinesReset == 1 && listItem.ColumnWidth < 0) + { + state.Close(listItem); + // Leave the list open + list.IsOpen = true; return BlockState.Continue; } - list.CountBlankLinesReset = 0; + // Update list-item source end position + listItem.UpdateSpanEnd(state.Line.End); + + return BlockState.Continue; + } + + list.CountBlankLinesReset = 0; - int columnWidth = listItem.ColumnWidth; - if (columnWidth < 0) + int columnWidth = listItem.ColumnWidth; + if (columnWidth < 0) + { + columnWidth = -columnWidth; + } + + if (state.Indent >= columnWidth) + { + if (state.Indent > columnWidth && state.IsCodeIndent) { - columnWidth = -columnWidth; + state.GoToColumn(state.ColumnBeforeIndent + columnWidth); } - if (state.Indent >= columnWidth) - { - if (state.Indent > columnWidth && state.IsCodeIndent) - { - state.GoToColumn(state.ColumnBeforeIndent + columnWidth); - } + // Update list-item source end position + listItem.UpdateSpanEnd(state.Line.End); + listItem.NewLine = state.Line.NewLine; - // Update list-item source end position - listItem.UpdateSpanEnd(state.Line.End); - listItem.NewLine = state.Line.NewLine; + return BlockState.Continue; + } - return BlockState.Continue; - } + return BlockState.None; + } + + private BlockState TryParseListItem(BlockProcessor state, Block? block) + { + var currentListItem = block as ListItemBlock; + var currentParent = block as ListBlock ?? (ListBlock)currentListItem?.Parent!; + // We can early exit if we have a code indent and we are either (1) not in a ListItem, (2) preceded by a blank line, (3) in an unordered list + if (state.IsCodeIndent && (currentListItem is null || currentListItem.LastChild is BlankLineBlock || !currentParent.IsOrdered)) + { return BlockState.None; } - private BlockState TryParseListItem(BlockProcessor state, Block? block) + var initColumnBeforeIndent = state.ColumnBeforeIndent; + var initColumn = state.Column; + var sourcePosition = state.Start; + var sourceEndPosition = state.Line.End; + + var c = state.CurrentChar; + var itemParser = mapItemParsers![c]; + if (itemParser is null) { - var currentListItem = block as ListItemBlock; - var currentParent = block as ListBlock ?? (ListBlock)currentListItem?.Parent!; + return BlockState.None; + } + + // Try to parse the list item + if (!itemParser.TryParse(state, currentParent?.BulletType ?? '\0', out ListInfo listInfo)) + { + // Reset to an a start position + state.GoToColumn(initColumn); + return BlockState.None; + } + var savedTriviaStart = state.TriviaStart; + var triviaBefore = state.UseTrivia(sourcePosition - 1); - // We can early exit if we have a code indent and we are either (1) not in a ListItem, (2) preceded by a blank line, (3) in an unordered list - if (state.IsCodeIndent && (currentListItem is null || currentListItem.LastChild is BlankLineBlock || !currentParent.IsOrdered)) + // set trivia to the mandatory whitespace after the bullet + state.TriviaStart = state.Start; + + bool isOrdered = itemParser is OrderedListItemParser; + + // Gets the current character after a successful parsing of the list information + c = state.CurrentChar; + + // Item starting with a blank line + int columnWidth; + + // Do we have a blank line right after the bullet? + if (c == '\0') + { + // Use a negative number to store the number of expected chars + columnWidth = -(state.Column - initColumnBeforeIndent + 1); + } + else + { + if (!c.IsSpaceOrTab()) { + state.GoToColumn(initColumn); + state.TriviaStart = savedTriviaStart; // restore changed TriviaStart state return BlockState.None; } - var initColumnBeforeIndent = state.ColumnBeforeIndent; - var initColumn = state.Column; - var sourcePosition = state.Start; - var sourceEndPosition = state.Line.End; + // Parse the following indent + state.RestartIndent(); + var columnBeforeIndent = state.Column; + state.ParseIndent(); - var c = state.CurrentChar; - var itemParser = mapItemParsers![c]; - if (itemParser is null) + // We expect at most 4 columns after + // If we have more, we reset the position + if (state.Indent > 4) { - return BlockState.None; + state.GoToColumn(columnBeforeIndent + 1); } - // Try to parse the list item - if (!itemParser.TryParse(state, currentParent?.BulletType ?? '\0', out ListInfo listInfo)) + // Number of spaces required for the following content to be part of this list item + // If the list item starts with a blank line, the number of spaces + // following the list marker doesn't change the required indentation + columnWidth = (state.IsBlankLine ? columnBeforeIndent : state.Column) - initColumnBeforeIndent; + } + + // Starts/continue the list unless: + // - an empty list item follows a paragraph + // - an ordered list is not starting by '1' + block ??= state.LastBlock; + if (block is not null && block.IsParagraphBlock) + { + if (state.IsBlankLine || + state.IsOpen(block) && listInfo.BulletType == '1' && listInfo.OrderedStart is not "1") { - // Reset to an a start position state.GoToColumn(initColumn); + state.TriviaStart = savedTriviaStart; // restore changed TriviaStart state return BlockState.None; } - var savedTriviaStart = state.TriviaStart; - var triviaBefore = state.UseTrivia(sourcePosition - 1); - - // set trivia to the mandatory whitespace after the bullet - state.TriviaStart = state.Start; + } - bool isOrdered = itemParser is OrderedListItemParser; + int.TryParse(listInfo.OrderedStart, out int order); + var newListItem = new ListItemBlock(this) + { + Column = initColumn, + ColumnWidth = columnWidth, + Order = order, + Span = new SourceSpan(sourcePosition, sourceEndPosition), + }; - // Gets the current character after a successful parsing of the list information - c = state.CurrentChar; + if (state.TrackTrivia) + { + newListItem.TriviaBefore = triviaBefore; + newListItem.LinesBefore = state.UseLinesBefore(); + newListItem.NewLine = state.Line.NewLine; + newListItem.SourceBullet = listInfo.SourceBullet; + } - // Item starting with a blank line - int columnWidth; + state.NewBlocks.Push(newListItem); - // Do we have a blank line right after the bullet? - if (c == '\0') - { - // Use a negative number to store the number of expected chars - columnWidth = -(state.Column - initColumnBeforeIndent + 1); - } - else + if (currentParent != null) + { + // If we have a new list item, close the previous one + if (currentListItem != null) { - if (!c.IsSpaceOrTab()) - { - state.GoToColumn(initColumn); - state.TriviaStart = savedTriviaStart; // restore changed TriviaStart state - return BlockState.None; - } - - // Parse the following indent - state.RestartIndent(); - var columnBeforeIndent = state.Column; - state.ParseIndent(); - - // We expect at most 4 columns after - // If we have more, we reset the position - if (state.Indent > 4) - { - state.GoToColumn(columnBeforeIndent + 1); - } - - // Number of spaces required for the following content to be part of this list item - // If the list item starts with a blank line, the number of spaces - // following the list marker doesn't change the required indentation - columnWidth = (state.IsBlankLine ? columnBeforeIndent : state.Column) - initColumnBeforeIndent; + state.Close(currentListItem); } - // Starts/continue the list unless: - // - an empty list item follows a paragraph - // - an ordered list is not starting by '1' - block ??= state.LastBlock; - if (block is not null && block.IsParagraphBlock) + // Reset the list if it is a new list or a new type of bullet + if (currentParent.IsOrdered != isOrdered || + currentParent.OrderedDelimiter != listInfo.OrderedDelimiter || + currentParent.BulletType != listInfo.BulletType) { - if (state.IsBlankLine || - state.IsOpen(block) && listInfo.BulletType == '1' && listInfo.OrderedStart is not "1") - { - state.GoToColumn(initColumn); - state.TriviaStart = savedTriviaStart; // restore changed TriviaStart state - return BlockState.None; - } + state.Close(currentParent); + currentParent = null; } + } - int.TryParse(listInfo.OrderedStart, out int order); - var newListItem = new ListItemBlock(this) + if (currentParent is null) + { + var newList = new ListBlock(this) { Column = initColumn, - ColumnWidth = columnWidth, - Order = order, Span = new SourceSpan(sourcePosition, sourceEndPosition), + IsOrdered = isOrdered, + BulletType = listInfo.BulletType, + OrderedDelimiter = listInfo.OrderedDelimiter, + DefaultOrderedStart = listInfo.DefaultOrderedStart, + OrderedStart = listInfo.OrderedStart, }; 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) - { - // If we have a new list item, close the previous one - if (currentListItem != null) - { - state.Close(currentListItem); - } - - // Reset the list if it is a new list or a new type of bullet - if (currentParent.IsOrdered != isOrdered || - currentParent.OrderedDelimiter != listInfo.OrderedDelimiter || - currentParent.BulletType != listInfo.BulletType) - { - state.Close(currentParent); - currentParent = null; - } + newList.LinesBefore = state.UseLinesBefore(); } - if (currentParent is null) - { - var newList = new ListBlock(this) - { - Column = initColumn, - Span = new SourceSpan(sourcePosition, sourceEndPosition), - IsOrdered = isOrdered, - BulletType = listInfo.BulletType, - OrderedDelimiter = listInfo.OrderedDelimiter, - DefaultOrderedStart = listInfo.DefaultOrderedStart, - OrderedStart = listInfo.OrderedStart, - }; - - if (state.TrackTrivia) - { - newList.LinesBefore = state.UseLinesBefore(); - } + state.NewBlocks.Push(newList); + } + return BlockState.Continue; + } - state.NewBlocks.Push(newList); - } - return BlockState.Continue; + public override bool Close(BlockProcessor processor, Block blockToClose) + { + if (processor.TrackTrivia) + { + return true; } - public override bool Close(BlockProcessor processor, Block blockToClose) + // Process only if we have blank lines + if (blockToClose is ListBlock listBlock && listBlock.CountAllBlankLines > 0) { - if (processor.TrackTrivia) + if (listBlock.Parent is ListItemBlock parentListItemBlock && + listBlock.LastChild is ListItemBlock lastListItem && + lastListItem.LastChild is BlankLineBlock) { - return true; + // Inform the outer list that we have a blank line + var parentList = (ListBlock)parentListItemBlock.Parent!; + + parentList.CountAllBlankLines++; + parentListItemBlock.Add(new BlankLineBlock()); } - // Process only if we have blank lines - if (blockToClose is ListBlock listBlock && listBlock.CountAllBlankLines > 0) + for (int listIndex = listBlock.Count - 1; listIndex >= 0; listIndex--) { - if (listBlock.Parent is ListItemBlock parentListItemBlock && - listBlock.LastChild is ListItemBlock lastListItem && - lastListItem.LastChild is BlankLineBlock) - { - // Inform the outer list that we have a blank line - var parentList = (ListBlock)parentListItemBlock.Parent!; - - parentList.CountAllBlankLines++; - parentListItemBlock.Add(new BlankLineBlock()); - } + var listItem = (ListItemBlock)listBlock[listIndex]; - for (int listIndex = listBlock.Count - 1; listIndex >= 0; listIndex--) + for (int i = listItem.Count - 1; i >= 0; i--) { - var listItem = (ListItemBlock)listBlock[listIndex]; - - for (int i = listItem.Count - 1; i >= 0; i--) + if (listItem[i] is BlankLineBlock) { - if (listItem[i] is BlankLineBlock) + if (i == listItem.Count - 1 ? listIndex < listBlock.Count - 1 : i > 0) { - if (i == listItem.Count - 1 ? listIndex < listBlock.Count - 1 : i > 0) - { - listBlock.IsLoose = true; - } - - listItem.RemoveAt(i); - - //If we have removed all blank lines, we can exit - listBlock.CountAllBlankLines--; - if (listBlock.CountAllBlankLines == 0) - { - goto done; - } + listBlock.IsLoose = true; + } + + listItem.RemoveAt(i); + + //If we have removed all blank lines, we can exit + listBlock.CountAllBlankLines--; + if (listBlock.CountAllBlankLines == 0) + { + goto done; } } } } - - done: - return true; } + + done: + return true; } } \ No newline at end of file diff --git a/src/Markdig/Parsers/ListInfo.cs b/src/Markdig/Parsers/ListInfo.cs index 20afa9454..bd802d39f 100644 --- a/src/Markdig/Parsers/ListInfo.cs +++ b/src/Markdig/Parsers/ListInfo.cs @@ -4,62 +4,61 @@ using Markdig.Helpers; -namespace Markdig.Parsers +namespace Markdig.Parsers; + +/// +/// Defines list information returned when trying to parse a list item with +/// +public struct ListInfo { /// - /// Defines list information returned when trying to parse a list item with + /// Initializes a new instance of the struct. /// - public struct ListInfo + /// Type of the bullet (e.g: '1', 'a', 'A', 'i', 'I'). + public ListInfo(char bulletType) { - /// - /// Initializes a new instance of the struct. - /// - /// Type of the bullet (e.g: '1', 'a', 'A', 'i', 'I'). - public ListInfo(char bulletType) - { - BulletType = bulletType; - OrderedStart = null; - OrderedDelimiter = (char)0; - DefaultOrderedStart = null; - SourceBullet = StringSlice.Empty; - } + BulletType = bulletType; + OrderedStart = null; + OrderedDelimiter = (char)0; + DefaultOrderedStart = null; + SourceBullet = StringSlice.Empty; + } - /// - /// Initializes a new instance of the struct. - /// - /// Type of the bullet (e.g: '1', 'a', 'A', 'i', 'I') - /// The string used as a starting sequence for an ordered list. - /// The ordered delimiter found when parsing this list (e.g: the character `)` after `1)`) - /// The default string used as a starting sequence for the ordered list (e.g: '1' for an numbered ordered list) - public ListInfo(char bulletType, string orderedStart, char orderedDelimiter, string defaultOrderedStart) - { - BulletType = bulletType; - OrderedStart = orderedStart; - OrderedDelimiter = orderedDelimiter; - DefaultOrderedStart = defaultOrderedStart; - SourceBullet = StringSlice.Empty; - } + /// + /// Initializes a new instance of the struct. + /// + /// Type of the bullet (e.g: '1', 'a', 'A', 'i', 'I') + /// The string used as a starting sequence for an ordered list. + /// The ordered delimiter found when parsing this list (e.g: the character `)` after `1)`) + /// The default string used as a starting sequence for the ordered list (e.g: '1' for an numbered ordered list) + public ListInfo(char bulletType, string orderedStart, char orderedDelimiter, string defaultOrderedStart) + { + BulletType = bulletType; + OrderedStart = orderedStart; + OrderedDelimiter = orderedDelimiter; + DefaultOrderedStart = defaultOrderedStart; + SourceBullet = StringSlice.Empty; + } - /// - /// Gets or sets the type of the bullet (e.g: '1', 'a', 'A', 'i', 'I'). - /// - public char BulletType { get; set; } + /// + /// Gets or sets the type of the bullet (e.g: '1', 'a', 'A', 'i', 'I'). + /// + public char BulletType { get; set; } - /// - /// Gets or sets the string used as a starting sequence for an ordered list - /// - public string? OrderedStart { get; set; } + /// + /// Gets or sets the string used as a starting sequence for an ordered list + /// + public string? OrderedStart { get; set; } - /// - /// Gets or sets the ordered delimiter found when parsing this list (e.g: the character `)` after `1)`) - /// - public char OrderedDelimiter { get; set; } + /// + /// Gets or sets the ordered delimiter found when parsing this list (e.g: the character `)` after `1)`) + /// + public char OrderedDelimiter { get; set; } - /// - /// Gets or sets default string used as a starting sequence for the ordered list (e.g: '1' for an numbered ordered list) - /// - public string? DefaultOrderedStart { get; set; } + /// + /// Gets or sets default string used as a starting sequence for the ordered list (e.g: '1' for an numbered ordered list) + /// + public string? DefaultOrderedStart { get; set; } - public StringSlice SourceBullet { get; set; } - } + public StringSlice SourceBullet { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Parsers/ListItemParser.cs b/src/Markdig/Parsers/ListItemParser.cs index d3e32a2e1..9de01a0f4 100644 --- a/src/Markdig/Parsers/ListItemParser.cs +++ b/src/Markdig/Parsers/ListItemParser.cs @@ -2,25 +2,24 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -namespace Markdig.Parsers +namespace Markdig.Parsers; + +/// +/// A parser base class for a list item. +/// +public abstract class ListItemParser { /// - /// A parser base class for a list item. + /// Defines the characters that are used for detecting this list item. /// - public abstract class ListItemParser - { - /// - /// Defines the characters that are used for detecting this list item. - /// - public char[]? OpeningCharacters { get; protected set; } + public char[]? OpeningCharacters { get; protected set; } - /// - /// Tries to parse the current input as a list item for this particular instance. - /// - /// The block processor - /// The type of the current bullet type - /// The result of parsing - /// true if parsing was successful; false otherwise - public abstract bool TryParse(BlockProcessor state, char pendingBulletType, out ListInfo result); - } + /// + /// Tries to parse the current input as a list item for this particular instance. + /// + /// The block processor + /// The type of the current bullet type + /// The result of parsing + /// true if parsing was successful; false otherwise + public abstract bool TryParse(BlockProcessor state, char pendingBulletType, out ListInfo result); } \ No newline at end of file diff --git a/src/Markdig/Parsers/MarkdownParser.cs b/src/Markdig/Parsers/MarkdownParser.cs index d81e25e58..48ecc13f7 100644 --- a/src/Markdig/Parsers/MarkdownParser.cs +++ b/src/Markdig/Parsers/MarkdownParser.cs @@ -2,216 +2,214 @@ // 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.Collections.Generic; using System.Runtime.CompilerServices; + using Markdig.Helpers; using Markdig.Syntax; -namespace Markdig.Parsers -{ - /// - /// Delegates called when processing a document - /// - /// The markdown document. - public delegate void ProcessDocumentDelegate(MarkdownDocument document); +namespace Markdig.Parsers; + +/// +/// Delegates called when processing a document +/// +/// The markdown document. +public delegate void ProcessDocumentDelegate(MarkdownDocument document); +/// +/// The Markdown parser. +/// +public static class MarkdownParser +{ /// - /// The Markdown parser. + /// Parses the specified markdown into an AST /// - public static class MarkdownParser + /// A Markdown text + /// The pipeline used for the parsing. + /// A parser context used for the parsing. + /// An AST Markdown document + /// if reader variable is null + public static MarkdownDocument Parse(string text, MarkdownPipeline? pipeline = null, MarkdownParserContext? context = null) { - /// - /// Parses the specified markdown into an AST - /// - /// A Markdown text - /// The pipeline used for the parsing. - /// A parser context used for the parsing. - /// An AST Markdown document - /// if reader variable is null - public static MarkdownDocument Parse(string text, MarkdownPipeline? pipeline = null, MarkdownParserContext? context = null) - { - if (text is null) ThrowHelper.ArgumentNullException_text(); + if (text is null) ThrowHelper.ArgumentNullException_text(); - pipeline ??= Markdown.DefaultPipeline; + pipeline ??= Markdown.DefaultPipeline; - text = FixupZero(text); + text = FixupZero(text); - var document = new MarkdownDocument - { - IsOpen = true - }; + var document = new MarkdownDocument + { + IsOpen = true + }; - if (pipeline.PreciseSourceLocation) - { - int roughLineCountEstimate = text.Length / 32; - roughLineCountEstimate = Math.Max(4, Math.Min(512, roughLineCountEstimate)); - document.LineStartIndexes = new List(roughLineCountEstimate); - } + if (pipeline.PreciseSourceLocation) + { + int roughLineCountEstimate = text.Length / 32; + roughLineCountEstimate = Math.Max(4, Math.Min(512, roughLineCountEstimate)); + document.LineStartIndexes = new List(roughLineCountEstimate); + } - var blockProcessor = BlockProcessor.Rent(document, pipeline.BlockParsers, context, pipeline.TrackTrivia); - try - { - blockProcessor.Open(document); + var blockProcessor = BlockProcessor.Rent(document, pipeline.BlockParsers, context, pipeline.TrackTrivia); + try + { + blockProcessor.Open(document); - ProcessBlocks(blockProcessor, new LineReader(text)); + ProcessBlocks(blockProcessor, new LineReader(text)); - if (pipeline.TrackTrivia) + if (pipeline.TrackTrivia) + { + Block? lastBlock = blockProcessor.LastBlock; + if (lastBlock is null && document.Count == 0) { - Block? lastBlock = blockProcessor.LastBlock; - if (lastBlock is null && document.Count == 0) + // this means we have unassigned characters + var noBlocksFoundBlock = new EmptyBlock(null); + List linesBefore = blockProcessor.UseLinesBefore(); + noBlocksFoundBlock.LinesAfter = new List(); + if (linesBefore != null) { - // this means we have unassigned characters - var noBlocksFoundBlock = new EmptyBlock(null); - List linesBefore = blockProcessor.UseLinesBefore(); - noBlocksFoundBlock.LinesAfter = new List(); - if (linesBefore != null) - { - noBlocksFoundBlock.LinesAfter.AddRange(linesBefore); - } - - document.Add(noBlocksFoundBlock); + noBlocksFoundBlock.LinesAfter.AddRange(linesBefore); } - else if (lastBlock != null && blockProcessor.LinesBefore != null) - { - // this means we're out of lines, but still have unassigned empty lines. - // thus, we'll assign the empty unsassigned lines to the last block - // of the document. - var rootMostContainerBlock = Block.FindRootMostContainerParent(lastBlock); - rootMostContainerBlock.LinesAfter ??= new List(); - var linesBefore = blockProcessor.UseLinesBefore(); - rootMostContainerBlock.LinesAfter.AddRange(linesBefore); - } - } - - // At this point the LineIndex is the same as the number of lines in the document - document.LineCount = blockProcessor.LineIndex; - } - finally - { - BlockProcessor.Release(blockProcessor); - } - var inlineProcessor = InlineProcessor.Rent(document, pipeline.InlineParsers, pipeline.PreciseSourceLocation, context, pipeline.TrackTrivia); - inlineProcessor.DebugLog = pipeline.DebugLog; - try - { - ProcessInlines(inlineProcessor, document); - } - finally - { - InlineProcessor.Release(inlineProcessor); + document.Add(noBlocksFoundBlock); + } + else if (lastBlock != null && blockProcessor.LinesBefore != null) + { + // this means we're out of lines, but still have unassigned empty lines. + // thus, we'll assign the empty unsassigned lines to the last block + // of the document. + var rootMostContainerBlock = Block.FindRootMostContainerParent(lastBlock); + rootMostContainerBlock.LinesAfter ??= new List(); + var linesBefore = blockProcessor.UseLinesBefore(); + rootMostContainerBlock.LinesAfter.AddRange(linesBefore); + } } - // Allow to call a hook after processing a document - pipeline.DocumentProcessed?.Invoke(document); - - return document; + // At this point the LineIndex is the same as the number of lines in the document + document.LineCount = blockProcessor.LineIndex; } - - /// - /// Fixups the zero character by replacing it to a secure character (Section 2.3 Insecure characters, CommonMark specs) - /// - /// The text to secure. - private static string FixupZero(string text) + finally { - return text.Replace('\0', CharHelper.ReplacementChar); + BlockProcessor.Release(blockProcessor); } - private static void ProcessBlocks(BlockProcessor blockProcessor, LineReader lineReader) + var inlineProcessor = InlineProcessor.Rent(document, pipeline.InlineParsers, pipeline.PreciseSourceLocation, context, pipeline.TrackTrivia); + inlineProcessor.DebugLog = pipeline.DebugLog; + try { - while (true) - { - // Get the precise position of the begining of the line - var lineText = lineReader.ReadLine(); + ProcessInlines(inlineProcessor, document); + } + finally + { + InlineProcessor.Release(inlineProcessor); + } - // If this is the end of file and the last line is empty - if (lineText.Text is null) - { - break; - } + // Allow to call a hook after processing a document + pipeline.DocumentProcessed?.Invoke(document); + + return document; + } + + /// + /// Fixups the zero character by replacing it to a secure character (Section 2.3 Insecure characters, CommonMark specs) + /// + /// The text to secure. + private static string FixupZero(string text) + { + return text.Replace('\0', CharHelper.ReplacementChar); + } + + private static void ProcessBlocks(BlockProcessor blockProcessor, LineReader lineReader) + { + while (true) + { + // Get the precise position of the begining of the line + var lineText = lineReader.ReadLine(); - blockProcessor.ProcessLine(lineText); + // If this is the end of file and the last line is empty + if (lineText.Text is null) + { + break; } - blockProcessor.CloseAll(true); + + blockProcessor.ProcessLine(lineText); } + blockProcessor.CloseAll(true); + } - private static void ProcessInlines(InlineProcessor inlineProcessor, MarkdownDocument document) - { - // "stackless" processor - int blockCount = 1; - var blocks = new ContainerItem[4]; + private static void ProcessInlines(InlineProcessor inlineProcessor, MarkdownDocument document) + { + // "stackless" processor + int blockCount = 1; + var blocks = new ContainerItem[4]; - blocks[0] = new ContainerItem(document); - document.OnProcessInlinesBegin(inlineProcessor); + blocks[0] = new ContainerItem(document); + document.OnProcessInlinesBegin(inlineProcessor); - while (blockCount != 0) - { - process_new_block: - ref ContainerItem item = ref blocks[blockCount - 1]; - var container = item.Container; + while (blockCount != 0) + { + process_new_block: + ref ContainerItem item = ref blocks[blockCount - 1]; + var container = item.Container; - for (; item.Index < container.Count; item.Index++) + for (; item.Index < container.Count; item.Index++) + { + var block = container[item.Index]; + if (block.IsLeafBlock) { - var block = container[item.Index]; - if (block.IsLeafBlock) + LeafBlock leafBlock = Unsafe.As(block); + leafBlock.OnProcessInlinesBegin(inlineProcessor); + if (leafBlock.ProcessInlines) { - LeafBlock leafBlock = Unsafe.As(block); - leafBlock.OnProcessInlinesBegin(inlineProcessor); - if (leafBlock.ProcessInlines) - { - inlineProcessor.ProcessInlineLeaf(leafBlock); - if (leafBlock.RemoveAfterProcessInlines) - { - container.RemoveAt(item.Index); - item.Index--; - } - else if (inlineProcessor.BlockNew != null) - { - container[item.Index] = inlineProcessor.BlockNew; - } - } - leafBlock.OnProcessInlinesEnd(inlineProcessor); - } - else if (block.IsContainerBlock) - { - // If we need to remove it - if (block.RemoveAfterProcessInlines) + inlineProcessor.ProcessInlineLeaf(leafBlock); + if (leafBlock.RemoveAfterProcessInlines) { container.RemoveAt(item.Index); + item.Index--; } - else + else if (inlineProcessor.BlockNew != null) { - // Else we have processed it - item.Index++; + container[item.Index] = inlineProcessor.BlockNew; } + } + leafBlock.OnProcessInlinesEnd(inlineProcessor); + } + else if (block.IsContainerBlock) + { + // If we need to remove it + if (block.RemoveAfterProcessInlines) + { + container.RemoveAt(item.Index); + } + else + { + // Else we have processed it + item.Index++; + } - if (blockCount == blocks.Length) - { - Array.Resize(ref blocks, blockCount * 2); - ThrowHelper.CheckDepthLimit(blocks.Length); - } - blocks[blockCount++] = new ContainerItem(Unsafe.As(block)); - block.OnProcessInlinesBegin(inlineProcessor); - goto process_new_block; + if (blockCount == blocks.Length) + { + Array.Resize(ref blocks, blockCount * 2); + ThrowHelper.CheckDepthLimit(blocks.Length); } + blocks[blockCount++] = new ContainerItem(Unsafe.As(block)); + block.OnProcessInlinesBegin(inlineProcessor); + goto process_new_block; } - container.OnProcessInlinesEnd(inlineProcessor); - blocks[--blockCount] = default; } + container.OnProcessInlinesEnd(inlineProcessor); + blocks[--blockCount] = default; } + } - private struct ContainerItem + private struct ContainerItem + { + public ContainerItem(ContainerBlock container) { - public ContainerItem(ContainerBlock container) - { - Container = container; - Index = 0; - } + Container = container; + Index = 0; + } - public readonly ContainerBlock Container; + public readonly ContainerBlock Container; - public int Index; - } + public int Index; } } \ No newline at end of file diff --git a/src/Markdig/Parsers/NumberedListItemParser.cs b/src/Markdig/Parsers/NumberedListItemParser.cs index 1df6bf0ce..cb55140c8 100644 --- a/src/Markdig/Parsers/NumberedListItemParser.cs +++ b/src/Markdig/Parsers/NumberedListItemParser.cs @@ -3,75 +3,73 @@ // See the license.txt file in the project root for more information. using Markdig.Helpers; -using System; -namespace Markdig.Parsers +namespace Markdig.Parsers; + +/// +/// The default parser for parsing numbered list item (e.g: 1) or 1.) +/// +/// +public class NumberedListItemParser : OrderedListItemParser { /// - /// The default parser for parsing numbered list item (e.g: 1) or 1.) + /// Initializes a new instance of the class. /// - /// - public class NumberedListItemParser : OrderedListItemParser + public NumberedListItemParser() { - /// - /// Initializes a new instance of the class. - /// - public NumberedListItemParser() + OpeningCharacters = new char[10]; + for (int i = 0; i < 10; i++) { - OpeningCharacters = new char[10]; - for (int i = 0; i < 10; i++) - { - OpeningCharacters[i] = (char) ('0' + i); - } + OpeningCharacters[i] = (char) ('0' + i); } + } - public override bool TryParse(BlockProcessor state, char pendingBulletType, out ListInfo result) - { - result = new ListInfo(); - var c = state.CurrentChar; - var sourcePosition = state.Start; + public override bool TryParse(BlockProcessor state, char pendingBulletType, out ListInfo result) + { + result = new ListInfo(); + var c = state.CurrentChar; + var sourcePosition = state.Start; - int countDigit = 0; - int startChar = -1; - int endChar = 0; - while (c.IsDigit()) - { - endChar = state.Start; - // Trim left 0 - if (startChar < 0 && c != '0') - { - startChar = endChar; - } - c = state.NextChar(); - countDigit++; - } - var sourceBullet = new StringSlice(state.Line.Text, sourcePosition, state.Start - 1); - if (startChar < 0) + int countDigit = 0; + int startChar = -1; + int endChar = 0; + while (c.IsDigit()) + { + endChar = state.Start; + // Trim left 0 + if (startChar < 0 && c != '0') { startChar = endChar; } + c = state.NextChar(); + countDigit++; + } + var sourceBullet = new StringSlice(state.Line.Text, sourcePosition, state.Start - 1); + if (startChar < 0) + { + startChar = endChar; + } - // Note that ordered list start numbers must be nine digits or less: - if (countDigit > 9 || !TryParseDelimiter(state, out char orderedDelimiter)) - { - return false; - } - - if (startChar == endChar) - { - // Common case: a single digit character - result.OrderedStart = CharHelper.SmallNumberToString(state.Line.Text[startChar] - '0'); - } - else - { - result.OrderedStart = state.Line.Text.Substring(startChar, endChar - startChar + 1); - } + // Note that ordered list start numbers must be nine digits or less: + if (countDigit > 9 || !TryParseDelimiter(state, out char orderedDelimiter)) + { + return false; + } - result.OrderedDelimiter = orderedDelimiter; - result.BulletType = '1'; - result.DefaultOrderedStart = "1"; - result.SourceBullet = sourceBullet; - return true; + if (startChar == endChar) + { + // Common case: a single digit character + result.OrderedStart = CharHelper.SmallNumberToString(state.Line.Text[startChar] - '0'); } + else + { + result.OrderedStart = state.Line.Text.Substring(startChar, endChar - startChar + 1); + } + + result.OrderedDelimiter = orderedDelimiter; + result.BulletType = '1'; + result.DefaultOrderedStart = "1"; + result.SourceBullet = sourceBullet; + return true; } } \ No newline at end of file diff --git a/src/Markdig/Parsers/OrderedListItemParser.cs b/src/Markdig/Parsers/OrderedListItemParser.cs index 23d694b83..8bf1e7626 100644 --- a/src/Markdig/Parsers/OrderedListItemParser.cs +++ b/src/Markdig/Parsers/OrderedListItemParser.cs @@ -2,46 +2,45 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -namespace Markdig.Parsers +namespace Markdig.Parsers; + +/// +/// Base class for an ordered list item parser. +/// +/// +public abstract class OrderedListItemParser : ListItemParser { /// - /// Base class for an ordered list item parser. + /// Initializes a new instance of the class. /// - /// - public abstract class OrderedListItemParser : ListItemParser + protected OrderedListItemParser() { - /// - /// Initializes a new instance of the class. - /// - protected OrderedListItemParser() - { - OrderedDelimiters = new[] { '.', ')' }; - } + OrderedDelimiters = new[] { '.', ')' }; + } - /// - /// Gets or sets the ordered delimiters used after a digit/number (by default `.` and `)`) - /// - public char[] OrderedDelimiters { get; set; } + /// + /// Gets or sets the ordered delimiters used after a digit/number (by default `.` and `)`) + /// + public char[] OrderedDelimiters { get; set; } - /// - /// Utility method that tries to parse the delimiter coming after an ordered list start (e.g: the `)` after `1)`). - /// - /// The state. - /// The ordered delimiter found if this method is successful. - /// true if parsing was successful; false otherwise. - protected bool TryParseDelimiter(BlockProcessor state, out char orderedDelimiter) + /// + /// Utility method that tries to parse the delimiter coming after an ordered list start (e.g: the `)` after `1)`). + /// + /// The state. + /// The ordered delimiter found if this method is successful. + /// true if parsing was successful; false otherwise. + protected bool TryParseDelimiter(BlockProcessor state, out char orderedDelimiter) + { + // Check if we have an ordered delimiter + orderedDelimiter = state.CurrentChar; + foreach (char delimiter in OrderedDelimiters) { - // Check if we have an ordered delimiter - orderedDelimiter = state.CurrentChar; - foreach (char delimiter in OrderedDelimiters) + if (delimiter == orderedDelimiter) { - if (delimiter == orderedDelimiter) - { - state.NextChar(); - return true; - } + state.NextChar(); + return true; } - return false; } + return false; } } \ No newline at end of file diff --git a/src/Markdig/Parsers/ParagraphBlockParser.cs b/src/Markdig/Parsers/ParagraphBlockParser.cs index f27e6e290..2fc050b97 100644 --- a/src/Markdig/Parsers/ParagraphBlockParser.cs +++ b/src/Markdig/Parsers/ParagraphBlockParser.cs @@ -5,284 +5,283 @@ using Markdig.Helpers; using Markdig.Syntax; -namespace Markdig.Parsers +namespace Markdig.Parsers; + +/// +/// Block parser for a . +/// +/// +public class ParagraphBlockParser : BlockParser { - /// - /// Block parser for a . - /// - /// - public class ParagraphBlockParser : BlockParser + public bool ParseSetexHeadings { get; set; } = true; + + public override BlockState TryOpen(BlockProcessor processor) { - public bool ParseSetexHeadings { get; set; } = true; + if (processor.IsBlankLine) + { + return BlockState.None; + } - public override BlockState TryOpen(BlockProcessor processor) + // We continue trying to match by default + var paragraph = new ParagraphBlock(this) { - if (processor.IsBlankLine) - { - return BlockState.None; - } + Column = processor.Column, + Span = new SourceSpan(processor.Line.Start, processor.Line.End), + }; - // We continue trying to match by default - var paragraph = new ParagraphBlock(this) - { - Column = processor.Column, - Span = new SourceSpan(processor.Line.Start, processor.Line.End), - }; + if (processor.TrackTrivia) + { + paragraph.LinesBefore = processor.UseLinesBefore(); + paragraph.NewLine = processor.Line.NewLine; + } - if (processor.TrackTrivia) - { - paragraph.LinesBefore = processor.UseLinesBefore(); - paragraph.NewLine = processor.Line.NewLine; - } + processor.NewBlocks.Push(paragraph); + return BlockState.Continue; + } - processor.NewBlocks.Push(paragraph); - return BlockState.Continue; + public override BlockState TryContinue(BlockProcessor processor, Block block) + { + if (processor.IsBlankLine) + { + return BlockState.BreakDiscard; } - public override BlockState TryContinue(BlockProcessor processor, Block block) + if (!processor.IsCodeIndent && ParseSetexHeadings) + { + return TryParseSetexHeading(processor, block); + } + block.NewLine = processor.Line.NewLine; + block.UpdateSpanEnd(processor.Line.End); + return BlockState.Continue; + } + + public override bool Close(BlockProcessor processor, Block block) + { + if (block is ParagraphBlock paragraph) { - if (processor.IsBlankLine) + ref var lines = ref paragraph.Lines; + + if (processor.TrackTrivia) { - return BlockState.BreakDiscard; + TryMatchLinkReferenceDefinitionTrivia(ref lines, processor, paragraph); } - - if (!processor.IsCodeIndent && ParseSetexHeadings) + else { - return TryParseSetexHeading(processor, block); + TryMatchLinkReferenceDefinition(ref lines, processor); } - block.NewLine = processor.Line.NewLine; - block.UpdateSpanEnd(processor.Line.End); - return BlockState.Continue; - } - public override bool Close(BlockProcessor processor, Block block) - { - if (block is ParagraphBlock paragraph) + int lineCount = lines.Count; + + // If Paragraph is empty, we can discard it + if (lineCount == 0) { - ref var lines = ref paragraph.Lines; + return false; + } - if (processor.TrackTrivia) - { - TryMatchLinkReferenceDefinitionTrivia(ref lines, processor, paragraph); - } - else + if (!processor.TrackTrivia) + { + for (int i = 0; i < lineCount; i++) { - TryMatchLinkReferenceDefinition(ref lines, processor); + lines.Lines[i].Slice.TrimStart(); } + lines.Lines[lineCount - 1].Slice.TrimEnd(); + } + } - int lineCount = lines.Count; + return true; + } - // If Paragraph is empty, we can discard it - if (lineCount == 0) - { - return false; - } + private BlockState TryParseSetexHeading(BlockProcessor state, Block block) + { + var line = state.Line; + var sourcePosition = line.Start; + int count = 0; + char headingChar = GetHeadingChar(ref line, ref count); - if (!processor.TrackTrivia) - { - for (int i = 0; i < lineCount; i++) - { - lines.Lines[i].Slice.TrimStart(); - } - lines.Lines[lineCount - 1].Slice.TrimEnd(); - } + if (headingChar != 0) + { + var paragraph = (ParagraphBlock)block; + + bool foundLrd; + if (state.TrackTrivia) + { + foundLrd = TryMatchLinkReferenceDefinitionTrivia(ref paragraph.Lines, state, paragraph); + } + else + { + foundLrd = TryMatchLinkReferenceDefinition(ref paragraph.Lines, state); } - return true; - } + // If we matched a LinkReferenceDefinition before matching the heading, and the remaining + // lines are empty, we can early exit and remove the paragraph + var parent = block.Parent; + bool isSetTextHeading = !state.IsLazy || paragraph.Column == state.Column || !(parent is QuoteBlock || parent is ListItemBlock); + if (!(foundLrd && paragraph.Lines.Count == 0) && isSetTextHeading) + { + // We discard the paragraph that will be transformed to a heading + state.Discard(paragraph); - private BlockState TryParseSetexHeading(BlockProcessor state, Block block) - { - var line = state.Line; - var sourcePosition = line.Start; - int count = 0; - char headingChar = GetHeadingChar(ref line, ref count); + while (state.CurrentChar == headingChar) + { + state.NextChar(); + } - if (headingChar != 0) - { - var paragraph = (ParagraphBlock)block; + int level = headingChar == '=' ? 1 : 2; + + var heading = new HeadingBlock(this) + { + Column = paragraph.Column, + Span = new SourceSpan(paragraph.Span.Start, line.Start), + Level = level, + Lines = paragraph.Lines, + IsSetext = true, + HeaderCharCount = count, + }; - bool foundLrd; if (state.TrackTrivia) { - foundLrd = TryMatchLinkReferenceDefinitionTrivia(ref paragraph.Lines, state, paragraph); + 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 { - foundLrd = TryMatchLinkReferenceDefinition(ref paragraph.Lines, state); + heading.Lines.Trim(); } - // If we matched a LinkReferenceDefinition before matching the heading, and the remaining - // lines are empty, we can early exit and remove the paragraph - var parent = block.Parent; - bool isSetTextHeading = !state.IsLazy || paragraph.Column == state.Column || !(parent is QuoteBlock || parent is ListItemBlock); - if (!(foundLrd && paragraph.Lines.Count == 0) && isSetTextHeading) - { - // We discard the paragraph that will be transformed to a heading - state.Discard(paragraph); - - while (state.CurrentChar == headingChar) - { - state.NextChar(); - } - - int level = headingChar == '=' ? 1 : 2; - - var heading = new HeadingBlock(this) - { - Column = paragraph.Column, - Span = new SourceSpan(paragraph.Span.Start, line.Start), - Level = level, - Lines = paragraph.Lines, - IsSetext = true, - HeaderCharCount = count, - }; - - 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(); - } - - // Remove the paragraph as a pending block - state.NewBlocks.Push(heading); - - return BlockState.BreakDiscard; - } + // Remove the paragraph as a pending block + state.NewBlocks.Push(heading); + + return BlockState.BreakDiscard; } + } - block.UpdateSpanEnd(state.Line.End); + block.UpdateSpanEnd(state.Line.End); - return BlockState.Continue; - } + return BlockState.Continue; + } + + private static char GetHeadingChar(ref StringSlice line, ref int count) + { + char headingChar = line.CurrentChar; - private static char GetHeadingChar(ref StringSlice line, ref int count) + if (headingChar == '=' || headingChar == '-') { - char headingChar = line.CurrentChar; + count = line.CountAndSkipChar(headingChar); - if (headingChar == '=' || headingChar == '-') + if (line.IsEmpty) { - count = line.CountAndSkipChar(headingChar); - - if (line.IsEmpty) - { - return headingChar; - } - - while (line.NextChar().IsSpaceOrTab()) - { - } + return headingChar; + } - if (line.IsEmpty) - { - return headingChar; - } + while (line.NextChar().IsSpaceOrTab()) + { } - return (char)0; + if (line.IsEmpty) + { + return headingChar; + } } - private static bool TryMatchLinkReferenceDefinition(ref StringLineGroup lines, BlockProcessor state) - { - bool atLeastOneFound = false; + return (char)0; + } - while (true) + private static bool TryMatchLinkReferenceDefinition(ref StringLineGroup lines, BlockProcessor state) + { + bool atLeastOneFound = false; + + while (true) + { + // If we have found a LinkReferenceDefinition, we can discard the previous paragraph + var iterator = lines.ToCharIterator(); + if (LinkReferenceDefinition.TryParse(ref iterator, out LinkReferenceDefinition? linkReferenceDefinition)) { - // If we have found a LinkReferenceDefinition, we can discard the previous paragraph - var iterator = lines.ToCharIterator(); - if (LinkReferenceDefinition.TryParse(ref iterator, out LinkReferenceDefinition? linkReferenceDefinition)) - { - state.Document.SetLinkReferenceDefinition(linkReferenceDefinition.Label!, linkReferenceDefinition, true); - atLeastOneFound = true; + state.Document.SetLinkReferenceDefinition(linkReferenceDefinition.Label!, linkReferenceDefinition, true); + atLeastOneFound = true; - // Correct the locations of each field - linkReferenceDefinition.Line = lines.Lines[0].Line; - int startPosition = lines.Lines[0].Slice.Start; + // Correct the locations of each field + linkReferenceDefinition.Line = lines.Lines[0].Line; + int startPosition = lines.Lines[0].Slice.Start; - linkReferenceDefinition.Span = linkReferenceDefinition.Span .MoveForward(startPosition); - linkReferenceDefinition.LabelSpan = linkReferenceDefinition.LabelSpan .MoveForward(startPosition); - linkReferenceDefinition.UrlSpan = linkReferenceDefinition.UrlSpan .MoveForward(startPosition); - linkReferenceDefinition.TitleSpan = linkReferenceDefinition.TitleSpan .MoveForward(startPosition); + linkReferenceDefinition.Span = linkReferenceDefinition.Span .MoveForward(startPosition); + linkReferenceDefinition.LabelSpan = linkReferenceDefinition.LabelSpan .MoveForward(startPosition); + linkReferenceDefinition.UrlSpan = linkReferenceDefinition.UrlSpan .MoveForward(startPosition); + linkReferenceDefinition.TitleSpan = linkReferenceDefinition.TitleSpan .MoveForward(startPosition); - lines = iterator.Remaining(); - } - else - { - break; - } + lines = iterator.Remaining(); + } + else + { + break; } - - return atLeastOneFound; } - private static bool TryMatchLinkReferenceDefinitionTrivia(ref StringLineGroup lines, BlockProcessor state, ParagraphBlock paragraph) - { - bool atLeastOneFound = false; + return atLeastOneFound; + } - while (true) + private static bool TryMatchLinkReferenceDefinitionTrivia(ref StringLineGroup lines, BlockProcessor state, ParagraphBlock paragraph) + { + bool atLeastOneFound = false; + + while (true) + { + // If we have found a LinkReferenceDefinition, we can discard the previous paragraph + var iterator = lines.ToCharIterator(); + if (LinkReferenceDefinition.TryParseTrivia( + ref iterator, + out LinkReferenceDefinition? lrd, + out SourceSpan triviaBeforeLabel, + out SourceSpan labelWithTrivia, + out SourceSpan triviaBeforeUrl, + out SourceSpan unescapedUrl, + out SourceSpan triviaBeforeTitle, + out SourceSpan unescapedTitle, + out SourceSpan triviaAfterTitle)) { - // If we have found a LinkReferenceDefinition, we can discard the previous paragraph - var iterator = lines.ToCharIterator(); - if (LinkReferenceDefinition.TryParseTrivia( - ref iterator, - out LinkReferenceDefinition? lrd, - out SourceSpan triviaBeforeLabel, - out SourceSpan labelWithTrivia, - out SourceSpan triviaBeforeUrl, - out SourceSpan unescapedUrl, - out SourceSpan triviaBeforeTitle, - out SourceSpan unescapedTitle, - out SourceSpan triviaAfterTitle)) - { - state.Document.SetLinkReferenceDefinition(lrd.Label!, lrd, false); - lrd.Parent = null; // remove LRDG parent from lrd - atLeastOneFound = true; - - // Correct the locations of each field - lrd.Line = lines.Lines[0].Line; - var text = lines.Lines[0].Slice.Text; - int startPosition = lines.Lines[0].Slice.Start; - - triviaBeforeLabel = triviaBeforeLabel.MoveForward(startPosition); - labelWithTrivia = labelWithTrivia.MoveForward(startPosition); - triviaBeforeUrl = triviaBeforeUrl.MoveForward(startPosition); - unescapedUrl = unescapedUrl.MoveForward(startPosition); - triviaBeforeTitle = triviaBeforeTitle.MoveForward(startPosition); - unescapedTitle = unescapedTitle.MoveForward(startPosition); - triviaAfterTitle = triviaAfterTitle.MoveForward(startPosition); - lrd.Span = lrd.Span.MoveForward(startPosition); - lrd.TriviaBefore = new StringSlice(text, triviaBeforeLabel.Start, triviaBeforeLabel.End); - lrd.LabelSpan = lrd.LabelSpan.MoveForward(startPosition); - lrd.LabelWithTrivia = new StringSlice(text, labelWithTrivia.Start, labelWithTrivia.End); - lrd.TriviaBeforeUrl = new StringSlice(text, triviaBeforeUrl.Start, triviaBeforeUrl.End); - lrd.UrlSpan = lrd.UrlSpan.MoveForward(startPosition); - lrd.UnescapedUrl = new StringSlice(text, unescapedUrl.Start, unescapedUrl.End); - lrd.TriviaBeforeTitle = new StringSlice(text, triviaBeforeTitle.Start, triviaBeforeTitle.End); - lrd.TitleSpan = lrd.TitleSpan.MoveForward(startPosition); - lrd.UnescapedTitle = new StringSlice(text, unescapedTitle.Start, unescapedTitle.End); - lrd.TriviaAfter = new StringSlice(text, triviaAfterTitle.Start, triviaAfterTitle.End); - lrd.LinesBefore = paragraph.LinesBefore; - - state.LinesBefore = paragraph.LinesAfter; // ensure closed paragraph with linesafter placed back on stack - - lines = iterator.Remaining(); - var index = paragraph.Parent!.IndexOf(paragraph); - paragraph.Parent.Insert(index, lrd); - } - else - { - break; - } + state.Document.SetLinkReferenceDefinition(lrd.Label!, lrd, false); + lrd.Parent = null; // remove LRDG parent from lrd + atLeastOneFound = true; + + // Correct the locations of each field + lrd.Line = lines.Lines[0].Line; + var text = lines.Lines[0].Slice.Text; + int startPosition = lines.Lines[0].Slice.Start; + + triviaBeforeLabel = triviaBeforeLabel.MoveForward(startPosition); + labelWithTrivia = labelWithTrivia.MoveForward(startPosition); + triviaBeforeUrl = triviaBeforeUrl.MoveForward(startPosition); + unescapedUrl = unescapedUrl.MoveForward(startPosition); + triviaBeforeTitle = triviaBeforeTitle.MoveForward(startPosition); + unescapedTitle = unescapedTitle.MoveForward(startPosition); + triviaAfterTitle = triviaAfterTitle.MoveForward(startPosition); + lrd.Span = lrd.Span.MoveForward(startPosition); + lrd.TriviaBefore = new StringSlice(text, triviaBeforeLabel.Start, triviaBeforeLabel.End); + lrd.LabelSpan = lrd.LabelSpan.MoveForward(startPosition); + lrd.LabelWithTrivia = new StringSlice(text, labelWithTrivia.Start, labelWithTrivia.End); + lrd.TriviaBeforeUrl = new StringSlice(text, triviaBeforeUrl.Start, triviaBeforeUrl.End); + lrd.UrlSpan = lrd.UrlSpan.MoveForward(startPosition); + lrd.UnescapedUrl = new StringSlice(text, unescapedUrl.Start, unescapedUrl.End); + lrd.TriviaBeforeTitle = new StringSlice(text, triviaBeforeTitle.Start, triviaBeforeTitle.End); + lrd.TitleSpan = lrd.TitleSpan.MoveForward(startPosition); + lrd.UnescapedTitle = new StringSlice(text, unescapedTitle.Start, unescapedTitle.End); + lrd.TriviaAfter = new StringSlice(text, triviaAfterTitle.Start, triviaAfterTitle.End); + lrd.LinesBefore = paragraph.LinesBefore; + + state.LinesBefore = paragraph.LinesAfter; // ensure closed paragraph with linesafter placed back on stack + + lines = iterator.Remaining(); + var index = paragraph.Parent!.IndexOf(paragraph); + paragraph.Parent.Insert(index, lrd); + } + else + { + break; } - - return atLeastOneFound; } + + return atLeastOneFound; } } \ No newline at end of file diff --git a/src/Markdig/Parsers/ParserBase.cs b/src/Markdig/Parsers/ParserBase.cs index cd90bcd51..f706a3113 100644 --- a/src/Markdig/Parsers/ParserBase.cs +++ b/src/Markdig/Parsers/ParserBase.cs @@ -2,30 +2,29 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -namespace Markdig.Parsers +namespace Markdig.Parsers; + +/// +/// Base class for a or . +/// +/// Type of the parser processor +/// +public abstract class ParserBase : IMarkdownParser { /// - /// Base class for a or . + /// Gets the opening characters this parser will be triggered if the character is found. /// - /// Type of the parser processor - /// - public abstract class ParserBase : IMarkdownParser - { - /// - /// Gets the opening characters this parser will be triggered if the character is found. - /// - public char[]? OpeningCharacters { get; set; } - - /// - /// Initializes this parser with the specified parser processor. - /// - public virtual void Initialize() - { - } + public char[]? OpeningCharacters { get; set; } - /// - /// Gets the index of this parser in or . - /// - public int Index { get; internal set; } + /// + /// Initializes this parser with the specified parser processor. + /// + public virtual void Initialize() + { } + + /// + /// Gets the index of this parser in or . + /// + public int Index { get; internal set; } } \ No newline at end of file diff --git a/src/Markdig/Parsers/ParserList.cs b/src/Markdig/Parsers/ParserList.cs index a9b6c286c..ec0db4942 100644 --- a/src/Markdig/Parsers/ParserList.cs +++ b/src/Markdig/Parsers/ParserList.cs @@ -2,120 +2,119 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using System.Collections.Generic; using System.Runtime.CompilerServices; + using Markdig.Helpers; -namespace Markdig.Parsers +namespace Markdig.Parsers; + +/// +/// Base class for a list of parsers. +/// +/// Type of the parser +/// The type of the parser state. +/// +public abstract class ParserList : OrderedList where T : notnull, ParserBase { - /// - /// Base class for a list of parsers. - /// - /// Type of the parser - /// The type of the parser state. - /// - public abstract class ParserList : OrderedList where T : notnull, ParserBase + private readonly CharacterMap charMap; + private readonly T[]? globalParsers; + + protected ParserList(IEnumerable parsersArg) : base(parsersArg) { - private readonly CharacterMap charMap; - private readonly T[]? globalParsers; + var charCounter = new Dictionary(); + int globalCounter = 0; - protected ParserList(IEnumerable parsersArg) : base(parsersArg) + for (int i = 0; i < Count; i++) { - var charCounter = new Dictionary(); - int globalCounter = 0; - - for (int i = 0; i < Count; i++) + var parser = this[i]; + if (parser is null) { - var parser = this[i]; - if (parser is null) - { - ThrowHelper.InvalidOperationException("Unexpected null parser found"); - } + ThrowHelper.InvalidOperationException("Unexpected null parser found"); + } - parser.Initialize(); - parser.Index = i; - if (parser.OpeningCharacters is { Length: > 0 }) + parser.Initialize(); + parser.Index = i; + if (parser.OpeningCharacters is { Length: > 0 }) + { + foreach (var openingChar in parser.OpeningCharacters) { - foreach (var openingChar in parser.OpeningCharacters) + if (!charCounter.ContainsKey(openingChar)) { - if (!charCounter.ContainsKey(openingChar)) - { - charCounter[openingChar] = 0; - } - charCounter[openingChar]++; + charCounter[openingChar] = 0; } - } - else - { - globalCounter++; + charCounter[openingChar]++; } } - - if (globalCounter > 0) + else { - globalParsers = new T[globalCounter]; + globalCounter++; } + } + + if (globalCounter > 0) + { + globalParsers = new T[globalCounter]; + } - var tempCharMap = new Dictionary(); - foreach (var parser in this) + var tempCharMap = new Dictionary(); + foreach (var parser in this) + { + if (parser.OpeningCharacters is { Length: > 0 }) { - if (parser.OpeningCharacters is { Length: > 0 }) + foreach (var openingChar in parser.OpeningCharacters) { - foreach (var openingChar in parser.OpeningCharacters) + if (!tempCharMap.TryGetValue(openingChar, out T[]? parsers)) { - if (!tempCharMap.TryGetValue(openingChar, out T[]? parsers)) - { - parsers = new T[charCounter[openingChar]]; - tempCharMap[openingChar] = parsers; - } - - var index = parsers.Length - charCounter[openingChar]; - parsers[index] = parser; - charCounter[openingChar]--; + parsers = new T[charCounter[openingChar]]; + tempCharMap[openingChar] = parsers; } - } - else - { - globalParsers![globalParsers.Length - globalCounter] = parser; - globalCounter--; + + var index = parsers.Length - charCounter[openingChar]; + parsers[index] = parser; + charCounter[openingChar]--; } } - - charMap = new CharacterMap(tempCharMap); + else + { + globalParsers![globalParsers.Length - globalCounter] = parser; + globalCounter--; + } } - /// - /// Gets the list of global parsers (that don't have any opening characters defined) - /// - public T[]? GlobalParsers => globalParsers; + charMap = new CharacterMap(tempCharMap); + } - /// - /// Gets all the opening characters defined. - /// - public char[] OpeningCharacters => charMap.OpeningCharacters; + /// + /// Gets the list of global parsers (that don't have any opening characters defined) + /// + public T[]? GlobalParsers => globalParsers; - /// - /// Gets the list of parsers valid for the specified opening character. - /// - /// The opening character. - /// A list of parsers valid for the specified opening character or null if no parsers registered. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public T[]? GetParsersForOpeningCharacter(uint openingChar) - { - return charMap[openingChar]; - } + /// + /// Gets all the opening characters defined. + /// + public char[] OpeningCharacters => charMap.OpeningCharacters; - /// - /// Searches for an opening character from a registered parser in the specified string. - /// - /// The text. - /// The start. - /// The end. - /// Index position within the string of the first opening character found in the specified text; if not found, returns -1 - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int IndexOfOpeningCharacter(string text, int start, int end) - { - return charMap.IndexOfOpeningCharacter(text, start, end); - } + /// + /// Gets the list of parsers valid for the specified opening character. + /// + /// The opening character. + /// A list of parsers valid for the specified opening character or null if no parsers registered. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T[]? GetParsersForOpeningCharacter(uint openingChar) + { + return charMap[openingChar]; + } + + /// + /// Searches for an opening character from a registered parser in the specified string. + /// + /// The text. + /// The start. + /// The end. + /// Index position within the string of the first opening character found in the specified text; if not found, returns -1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int IndexOfOpeningCharacter(string text, int start, int end) + { + return charMap.IndexOfOpeningCharacter(text, start, end); } } \ No newline at end of file diff --git a/src/Markdig/Parsers/QuoteBlockParser.cs b/src/Markdig/Parsers/QuoteBlockParser.cs index 7b6bc96b4..7cca8c99c 100644 --- a/src/Markdig/Parsers/QuoteBlockParser.cs +++ b/src/Markdig/Parsers/QuoteBlockParser.cs @@ -5,166 +5,165 @@ using Markdig.Helpers; using Markdig.Syntax; -namespace Markdig.Parsers +namespace Markdig.Parsers; + +/// +/// A block parser for a . +/// +/// +public class QuoteBlockParser : BlockParser { /// - /// A block parser for a . + /// Initializes a new instance of the class. /// - /// - public class QuoteBlockParser : BlockParser + public QuoteBlockParser() + { + OpeningCharacters = new[] {'>'}; + } + + public override BlockState TryOpen(BlockProcessor processor) { - /// - /// Initializes a new instance of the class. - /// - public QuoteBlockParser() + if (processor.IsCodeIndent) { - OpeningCharacters = new[] {'>'}; + return BlockState.None; } - public override BlockState TryOpen(BlockProcessor processor) - { - if (processor.IsCodeIndent) - { - return BlockState.None; - } + var sourcePosition = processor.Start; - var sourcePosition = processor.Start; + // 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 quoteChar = processor.CurrentChar; + var column = processor.Column; + var c = processor.NextChar(); - // 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 quoteChar = processor.CurrentChar; - var column = processor.Column; - var c = processor.NextChar(); + var quoteBlock = new QuoteBlock(this) + { + QuoteChar = quoteChar, + Column = column, + Span = new SourceSpan(sourcePosition, processor.Line.End), + }; - var quoteBlock = new QuoteBlock(this) - { - QuoteChar = quoteChar, - Column = column, - Span = new SourceSpan(sourcePosition, processor.Line.End), - }; + if (processor.TrackTrivia) + { + quoteBlock.LinesBefore = processor.UseLinesBefore(); + } - if (processor.TrackTrivia) - { - quoteBlock.LinesBefore = processor.UseLinesBefore(); - } + bool hasSpaceAfterQuoteChar = false; + if (c == ' ') + { + processor.NextColumn(); + hasSpaceAfterQuoteChar = true; + processor.SkipFirstUnwindSpace = true; + } + else if (c == '\t') + { + processor.NextColumn(); + } - bool hasSpaceAfterQuoteChar = false; - if (c == ' ') + if (processor.TrackTrivia) + { + var triviaBefore = processor.UseTrivia(sourcePosition - 1); + StringSlice triviaAfter = StringSlice.Empty; + bool wasEmptyLine = false; + if (processor.Line.IsEmptyOrWhitespace()) { - processor.NextColumn(); - hasSpaceAfterQuoteChar = true; - processor.SkipFirstUnwindSpace = true; + processor.TriviaStart = processor.Start; + triviaAfter = processor.UseTrivia(processor.Line.End); + wasEmptyLine = true; } - else if (c == '\t') + + if (!wasEmptyLine) { - processor.NextColumn(); + processor.TriviaStart = processor.Start; } - if (processor.TrackTrivia) + quoteBlock.QuoteLines.Add(new QuoteBlockLine { - 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; - } + 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); + return BlockState.Continue; + } - processor.NewBlocks.Push(quoteBlock); - return BlockState.Continue; + public override BlockState TryContinue(BlockProcessor processor, Block block) + { + if (processor.IsCodeIndent) + { + return BlockState.None; } - public override BlockState TryContinue(BlockProcessor processor, Block block) + var quote = (QuoteBlock) block; + var sourcePosition = processor.Start; + + // 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; + if (c != quote.QuoteChar) { - if (processor.IsCodeIndent) + if (processor.IsBlankLine) { - return BlockState.None; + return BlockState.BreakDiscard; } - - var quote = (QuoteBlock) block; - var sourcePosition = processor.Start; - - // 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; - if (c != quote.QuoteChar) + else { - if (processor.IsBlankLine) - { - return BlockState.BreakDiscard; - } - else + if (processor.TrackTrivia) { - if (processor.TrackTrivia) + quote.QuoteLines.Add(new QuoteBlockLine { - quote.QuoteLines.Add(new QuoteBlockLine - { - QuoteChar = false, - NewLine = processor.Line.NewLine, - }); - } - return BlockState.None; + QuoteChar = false, + NewLine = processor.Line.NewLine, + }); } + return BlockState.None; } + } - bool hasSpaceAfterQuoteChar = false; - c = processor.NextChar(); // Skip quote marker char - if (c == ' ') + bool hasSpaceAfterQuoteChar = false; + c = processor.NextChar(); // Skip quote marker char + if (c == ' ') + { + processor.NextColumn(); + hasSpaceAfterQuoteChar = true; + processor.SkipFirstUnwindSpace = true; + } + else if (c == '\t') + { + processor.NextColumn(); + } + + if (processor.TrackTrivia) + { + var triviaSpaceBefore = processor.UseTrivia(sourcePosition - 1); + StringSlice triviaAfter = StringSlice.Empty; + bool wasEmptyLine = false; + if (processor.Line.IsEmptyOrWhitespace()) { - processor.NextColumn(); - hasSpaceAfterQuoteChar = true; - processor.SkipFirstUnwindSpace = true; + processor.TriviaStart = processor.Start; + triviaAfter = processor.UseTrivia(processor.Line.End); + wasEmptyLine = true; } - else if (c == '\t') + quote.QuoteLines.Add(new QuoteBlockLine { - processor.NextColumn(); - } - - if (processor.TrackTrivia) + QuoteChar = true, + HasSpaceAfterQuoteChar = hasSpaceAfterQuoteChar, + TriviaBefore = triviaSpaceBefore, + TriviaAfter = triviaAfter, + NewLine = processor.Line.NewLine, + }); + + if (!wasEmptyLine) { - 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; - } + processor.TriviaStart = processor.Start; } - - block.UpdateSpanEnd(processor.Line.End); - return BlockState.Continue; } + + block.UpdateSpanEnd(processor.Line.End); + return BlockState.Continue; } } \ No newline at end of file diff --git a/src/Markdig/Parsers/ThematicBreakParser.cs b/src/Markdig/Parsers/ThematicBreakParser.cs index 9ccfa994e..343329029 100644 --- a/src/Markdig/Parsers/ThematicBreakParser.cs +++ b/src/Markdig/Parsers/ThematicBreakParser.cs @@ -5,106 +5,105 @@ using Markdig.Helpers; using Markdig.Syntax; -namespace Markdig.Parsers +namespace Markdig.Parsers; + +/// +/// A block parser for a . +/// +/// +public class ThematicBreakParser : BlockParser { /// - /// A block parser for a . + /// A singleton instance used by other parsers. /// - /// - public class ThematicBreakParser : BlockParser + public static readonly ThematicBreakParser Default = new ThematicBreakParser(); + + /// + /// Initializes a new instance of the class. + /// + public ThematicBreakParser() { - /// - /// A singleton instance used by other parsers. - /// - public static readonly ThematicBreakParser Default = new ThematicBreakParser(); + OpeningCharacters = new[] {'-', '_', '*'}; + } - /// - /// Initializes a new instance of the class. - /// - public ThematicBreakParser() + public override BlockState TryOpen(BlockProcessor processor) + { + if (processor.IsCodeIndent) { - OpeningCharacters = new[] {'-', '_', '*'}; + return BlockState.None; } - public override BlockState TryOpen(BlockProcessor processor) - { - if (processor.IsCodeIndent) - { - return BlockState.None; - } - - var startPosition = processor.Start; - var line = processor.Line; + var startPosition = processor.Start; + var line = processor.Line; - // 4.1 Thematic breaks - // A line consisting of 0-3 spaces of indentation, followed by a sequence of three or more matching -, _, or * characters, each followed optionally by any number of spaces - int breakCharCount = 0; - var breakChar = line.CurrentChar; - bool hasSpacesSinceLastMatch = false; - bool hasInnerSpaces = false; - var c = breakChar; - while (c != '\0') + // 4.1 Thematic breaks + // A line consisting of 0-3 spaces of indentation, followed by a sequence of three or more matching -, _, or * characters, each followed optionally by any number of spaces + int breakCharCount = 0; + var breakChar = line.CurrentChar; + bool hasSpacesSinceLastMatch = false; + bool hasInnerSpaces = false; + var c = breakChar; + while (c != '\0') + { + if (c == breakChar) { - if (c == breakChar) - { - if (hasSpacesSinceLastMatch) - { - hasInnerSpaces = true; - } - - breakCharCount++; - } - else if (c.IsSpaceOrTab()) - { - hasSpacesSinceLastMatch = true; - } - else + if (hasSpacesSinceLastMatch) { - return BlockState.None; + hasInnerSpaces = true; } - c = line.NextChar(); + breakCharCount++; } - - // If it as less than 3 chars or it is a setex heading and we are already in a paragraph, let the paragraph handle it - var previousParagraph = processor.CurrentBlock as ParagraphBlock; - - var isSetexHeading = previousParagraph != null && breakChar == '-' && !hasInnerSpaces; - if (isSetexHeading) + else if (c.IsSpaceOrTab()) { - var parent = previousParagraph!.Parent!; - if (previousParagraph.Column != processor.Column && (parent is QuoteBlock or ListItemBlock)) - { - isSetexHeading = false; - } + hasSpacesSinceLastMatch = true; } - - if (breakCharCount < 3 || isSetexHeading) + else { return BlockState.None; } - // Push a new block - var thematicBreak = new ThematicBreakBlock(this) - { - Column = processor.Column, - Span = new SourceSpan(startPosition, line.End), - ThematicChar = breakChar, - ThematicCharCount = breakCharCount, - // TODO: should we separate whitespace before/after? - //BeforeWhitespace = beforeWhitespace, - //AfterWhitespace = processor.PopBeforeWhitespace(processor.CurrentLineStartPosition), - Content = new StringSlice(line.Text, processor.TriviaStart, line.End, line.NewLine), //include whitespace for now - }; + c = line.NextChar(); + } - if (processor.TrackTrivia) + // If it as less than 3 chars or it is a setex heading and we are already in a paragraph, let the paragraph handle it + var previousParagraph = processor.CurrentBlock as ParagraphBlock; + + var isSetexHeading = previousParagraph != null && breakChar == '-' && !hasInnerSpaces; + if (isSetexHeading) + { + var parent = previousParagraph!.Parent!; + if (previousParagraph.Column != processor.Column && (parent is QuoteBlock or ListItemBlock)) { - thematicBreak.LinesBefore = processor.UseLinesBefore(); - thematicBreak.NewLine = processor.Line.NewLine; + isSetexHeading = false; } + } + + if (breakCharCount < 3 || isSetexHeading) + { + return BlockState.None; + } - processor.NewBlocks.Push(thematicBreak); - return BlockState.BreakDiscard; + // Push a new block + var thematicBreak = new ThematicBreakBlock(this) + { + Column = processor.Column, + Span = new SourceSpan(startPosition, line.End), + ThematicChar = breakChar, + ThematicCharCount = breakCharCount, + // TODO: should we separate whitespace before/after? + //BeforeWhitespace = beforeWhitespace, + //AfterWhitespace = processor.PopBeforeWhitespace(processor.CurrentLineStartPosition), + Content = new StringSlice(line.Text, processor.TriviaStart, line.End, line.NewLine), //include whitespace for now + }; + + if (processor.TrackTrivia) + { + thematicBreak.LinesBefore = processor.UseLinesBefore(); + thematicBreak.NewLine = processor.Line.NewLine; } + + processor.NewBlocks.Push(thematicBreak); + return BlockState.BreakDiscard; } } \ No newline at end of file diff --git a/src/Markdig/Parsers/UnorderedListItemParser.cs b/src/Markdig/Parsers/UnorderedListItemParser.cs index 5a8e20605..73cf85d6f 100644 --- a/src/Markdig/Parsers/UnorderedListItemParser.cs +++ b/src/Markdig/Parsers/UnorderedListItemParser.cs @@ -2,27 +2,26 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -namespace Markdig.Parsers +namespace Markdig.Parsers; + +/// +/// The default parser used to parse unordered list item (-, +, *) +/// +/// +public class UnorderedListItemParser : ListItemParser { /// - /// The default parser used to parse unordered list item (-, +, *) + /// Initializes a new instance of the class. /// - /// - public class UnorderedListItemParser : ListItemParser + public UnorderedListItemParser() { - /// - /// Initializes a new instance of the class. - /// - public UnorderedListItemParser() - { - OpeningCharacters = new [] {'-', '+', '*'}; - } + OpeningCharacters = new [] {'-', '+', '*'}; + } - public override bool TryParse(BlockProcessor state, char pendingBulletType, out ListInfo result) - { - result = new ListInfo(state.CurrentChar); - state.NextChar(); - return true; - } + public override bool TryParse(BlockProcessor state, char pendingBulletType, out ListInfo result) + { + result = new ListInfo(state.CurrentChar); + state.NextChar(); + return true; } } \ No newline at end of file diff --git a/src/Markdig/Polyfills/ConcurrentQueue.cs b/src/Markdig/Polyfills/ConcurrentQueue.cs index 864372933..70413d589 100644 --- a/src/Markdig/Polyfills/ConcurrentQueue.cs +++ b/src/Markdig/Polyfills/ConcurrentQueue.cs @@ -3,14 +3,13 @@ // See the license.txt file in the project root for more information. #if NETFRAMEWORK || NETSTANDARD2_0 -namespace System.Collections.Concurrent +namespace System.Collections.Concurrent; + +internal static class ConcurrentQueueExtensions { - internal static class ConcurrentQueueExtensions + public static void Clear(this ConcurrentQueue queue) { - public static void Clear(this ConcurrentQueue queue) - { - while (queue.TryDequeue(out _)) { } - } + while (queue.TryDequeue(out _)) { } } } #endif \ No newline at end of file diff --git a/src/Markdig/Polyfills/NullableAttributes.cs b/src/Markdig/Polyfills/NullableAttributes.cs index 632ceb7c4..c7eff84f3 100644 --- a/src/Markdig/Polyfills/NullableAttributes.cs +++ b/src/Markdig/Polyfills/NullableAttributes.cs @@ -3,31 +3,30 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace System.Diagnostics.CodeAnalysis -{ +namespace System.Diagnostics.CodeAnalysis; + #if !NETSTANDARD2_1_OR_GREATER && !NETCOREAPP3_1_OR_GREATER - internal sealed class DoesNotReturnAttribute : Attribute { } +internal sealed class DoesNotReturnAttribute : Attribute { } - [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] - internal sealed class NotNullWhenAttribute : Attribute - { - public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; +[AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +internal sealed class NotNullWhenAttribute : Attribute +{ + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; - public bool ReturnValue { get; } - } + public bool ReturnValue { get; } +} - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, Inherited = false)] - public sealed class AllowNullAttribute : Attribute { } +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, Inherited = false)] +public sealed class AllowNullAttribute : Attribute { } #endif #if !NET5_0_OR_GREATER - internal sealed class MemberNotNullAttribute : Attribute - { - public MemberNotNullAttribute(string member) => Members = new[] { member }; +internal sealed class MemberNotNullAttribute : Attribute +{ + public MemberNotNullAttribute(string member) => Members = new[] { member }; - public MemberNotNullAttribute(params string[] members) => Members = members; + public MemberNotNullAttribute(params string[] members) => Members = members; - public string[] Members { get; } - } -#endif + public string[] Members { get; } } +#endif diff --git a/src/Markdig/Polyfills/Unsafe.cs b/src/Markdig/Polyfills/Unsafe.cs index 5fc650b83..1ee72edc9 100644 --- a/src/Markdig/Polyfills/Unsafe.cs +++ b/src/Markdig/Polyfills/Unsafe.cs @@ -2,16 +2,15 @@ // 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 -{ +namespace System.Runtime.CompilerServices; + #if NETSTANDARD2_1 - internal static class Unsafe +internal static class Unsafe +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T As(object o) where T : class { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static T As(object o) where T : class - { - return (T)o; - } + return (T)o; } -#endif } +#endif \ No newline at end of file diff --git a/src/Markdig/Renderers/Html/CodeBlockRenderer.cs b/src/Markdig/Renderers/Html/CodeBlockRenderer.cs index c1599c29e..87bb1a435 100644 --- a/src/Markdig/Renderers/Html/CodeBlockRenderer.cs +++ b/src/Markdig/Renderers/Html/CodeBlockRenderer.cs @@ -2,90 +2,87 @@ // 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.Collections.Generic; using Markdig.Parsers; using Markdig.Syntax; -namespace Markdig.Renderers.Html +namespace Markdig.Renderers.Html; + +/// +/// An HTML renderer for a and . +/// +/// +public class CodeBlockRenderer : HtmlObjectRenderer { + private HashSet? _blocksAsDiv; + /// - /// An HTML renderer for a and . + /// Initializes a new instance of the class. /// - /// - public class CodeBlockRenderer : HtmlObjectRenderer - { - private HashSet? _blocksAsDiv; + public CodeBlockRenderer() { } - /// - /// Initializes a new instance of the class. - /// - public CodeBlockRenderer() { } + public bool OutputAttributesOnPre { get; set; } - public bool OutputAttributesOnPre { get; set; } + /// + /// Gets a map of fenced code block infos that should be rendered as div blocks instead of pre/code blocks. + /// + public HashSet BlocksAsDiv => _blocksAsDiv ??= new HashSet(StringComparer.OrdinalIgnoreCase); - /// - /// Gets a map of fenced code block infos that should be rendered as div blocks instead of pre/code blocks. - /// - public HashSet BlocksAsDiv => _blocksAsDiv ??= new HashSet(StringComparer.OrdinalIgnoreCase); + protected override void Write(HtmlRenderer renderer, CodeBlock obj) + { + renderer.EnsureLine(); - protected override void Write(HtmlRenderer renderer, CodeBlock obj) + if (_blocksAsDiv is not null && (obj as FencedCodeBlock)?.Info is string info && _blocksAsDiv.Contains(info)) { - renderer.EnsureLine(); - - if (_blocksAsDiv is not null && (obj as FencedCodeBlock)?.Info is string info && _blocksAsDiv.Contains(info)) - { - var infoPrefix = (obj.Parser as FencedCodeBlockParser)?.InfoPrefix ?? - FencedCodeBlockParser.DefaultInfoPrefix; + var infoPrefix = (obj.Parser as FencedCodeBlockParser)?.InfoPrefix ?? + FencedCodeBlockParser.DefaultInfoPrefix; - // We are replacing the HTML attribute `language-mylang` by `mylang` only for a div block - // NOTE that we are allocating a closure here + // We are replacing the HTML attribute `language-mylang` by `mylang` only for a div block + // NOTE that we are allocating a closure here - if (renderer.EnableHtmlForBlock) - { - renderer.Write(" cls.StartsWith(infoPrefix, StringComparison.Ordinal) ? cls.Substring(infoPrefix.Length) : cls) - .WriteRaw('>'); - } + if (renderer.EnableHtmlForBlock) + { + renderer.Write(" cls.StartsWith(infoPrefix, StringComparison.Ordinal) ? cls.Substring(infoPrefix.Length) : cls) + .WriteRaw('>'); + } - renderer.WriteLeafRawLines(obj, true, true, true); + renderer.WriteLeafRawLines(obj, true, true, true); - if (renderer.EnableHtmlForBlock) - { - renderer.WriteLine(""); - } + if (renderer.EnableHtmlForBlock) + { + renderer.WriteLine(""); } - else + } + else + { + if (renderer.EnableHtmlForBlock) { - if (renderer.EnableHtmlForBlock) - { - renderer.Write("'); + if (OutputAttributesOnPre) + { + renderer.WriteAttributes(obj); } - renderer.WriteLeafRawLines(obj, true, true); + renderer.WriteRaw(">"); + renderer.WriteAttributes(obj); } + + renderer.WriteRaw('>'); } - renderer.EnsureLine(); + renderer.WriteLeafRawLines(obj, true, true); + + if (renderer.EnableHtmlForBlock) + { + renderer.WriteLine(""); + } } + + renderer.EnsureLine(); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Html/HeadingRenderer.cs b/src/Markdig/Renderers/Html/HeadingRenderer.cs index d19e64b3a..e676bc081 100644 --- a/src/Markdig/Renderers/Html/HeadingRenderer.cs +++ b/src/Markdig/Renderers/Html/HeadingRenderer.cs @@ -4,49 +4,48 @@ using Markdig.Syntax; -namespace Markdig.Renderers.Html +namespace Markdig.Renderers.Html; + +/// +/// An HTML renderer for a . +/// +/// +public class HeadingRenderer : HtmlObjectRenderer { - /// - /// An HTML renderer for a . - /// - /// - public class HeadingRenderer : HtmlObjectRenderer + private static readonly string[] HeadingTexts = { + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + }; + + protected override void Write(HtmlRenderer renderer, HeadingBlock obj) { - private static readonly string[] HeadingTexts = { - "h1", - "h2", - "h3", - "h4", - "h5", - "h6", - }; - - protected override void Write(HtmlRenderer renderer, HeadingBlock obj) + int index = obj.Level - 1; + string[] headings = HeadingTexts; + string headingText = ((uint)index < (uint)headings.Length) + ? headings[index] + : $"h{obj.Level}"; + + if (renderer.EnableHtmlForBlock) + { + renderer.Write('<'); + renderer.WriteRaw(headingText); + renderer.WriteAttributes(obj); + renderer.WriteRaw('>'); + } + + renderer.WriteLeafInline(obj); + + if (renderer.EnableHtmlForBlock) { - int index = obj.Level - 1; - string[] headings = HeadingTexts; - string headingText = ((uint)index < (uint)headings.Length) - ? headings[index] - : $"h{obj.Level}"; - - if (renderer.EnableHtmlForBlock) - { - renderer.Write('<'); - renderer.WriteRaw(headingText); - renderer.WriteAttributes(obj); - renderer.WriteRaw('>'); - } - - renderer.WriteLeafInline(obj); - - if (renderer.EnableHtmlForBlock) - { - renderer.Write("'); - } - - renderer.EnsureLine(); + renderer.Write("'); } + + renderer.EnsureLine(); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Html/HtmlAttributes.cs b/src/Markdig/Renderers/Html/HtmlAttributes.cs index b8b6b89f4..ecba0380c 100644 --- a/src/Markdig/Renderers/Html/HtmlAttributes.cs +++ b/src/Markdig/Renderers/Html/HtmlAttributes.cs @@ -2,183 +2,181 @@ // 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.Collections.Generic; using System.Globalization; + using Markdig.Helpers; using Markdig.Syntax; -namespace Markdig.Renderers.Html +namespace Markdig.Renderers.Html; + +/// +/// Attached HTML attributes to a . +/// +public class HtmlAttributes : MarkdownObject { /// - /// Attached HTML attributes to a . + /// Initializes a new instance of the class. /// - public class HtmlAttributes : MarkdownObject + public HtmlAttributes() { - /// - /// Initializes a new instance of the class. - /// - public HtmlAttributes() - { - } + } - /// - /// Gets or sets the HTML id/identifier. May be null. - /// - public string? Id { get; set; } - - /// - /// Gets or sets the CSS classes attached. May be null. - /// - public List? Classes { get; set; } - - /// - /// Gets or sets the additional properties. May be null. - /// - public List>? Properties { get; set; } - - /// - /// Adds a CSS class. - /// - /// The css class name. - public void AddClass(string name) + /// + /// Gets or sets the HTML id/identifier. May be null. + /// + public string? Id { get; set; } + + /// + /// Gets or sets the CSS classes attached. May be null. + /// + public List? Classes { get; set; } + + /// + /// Gets or sets the additional properties. May be null. + /// + public List>? Properties { get; set; } + + /// + /// Adds a CSS class. + /// + /// The css class name. + public void AddClass(string name) + { + if (name is null) ThrowHelper.ArgumentNullException_name(); + + Classes ??= new (2);// Use half list compare to default capacity (4), as we don't expect lots of classes + + if (!Classes.Contains(name)) { - if (name is null) ThrowHelper.ArgumentNullException_name(); - - Classes ??= new (2);// Use half list compare to default capacity (4), as we don't expect lots of classes - - if (!Classes.Contains(name)) - { - Classes.Add(name); - } + Classes.Add(name); } + } + + /// + /// Adds a property. + /// + /// The name. + /// The value. + public void AddProperty(string name, string value) + { + if (name is null) ThrowHelper.ArgumentNullException_name(); + + Properties ??= new (2); // Use half list compare to default capacity (4), as we don't expect lots of classes + + Properties.Add(new KeyValuePair(name, value)); + } - /// - /// Adds a property. - /// - /// The name. - /// The value. - public void AddProperty(string name, string value) + /// + /// Adds the specified property only if it does not already exist. + /// + /// The name. + /// The value. + public void AddPropertyIfNotExist(string name, object? value) + { + if (name is null) ThrowHelper.ArgumentNullException_name(); + if (Properties is null) { - if (name is null) ThrowHelper.ArgumentNullException_name(); - - Properties ??= new (2); // Use half list compare to default capacity (4), as we don't expect lots of classes - - Properties.Add(new KeyValuePair(name, value)); + Properties = new (4); } - - /// - /// Adds the specified property only if it does not already exist. - /// - /// The name. - /// The value. - public void AddPropertyIfNotExist(string name, object? value) + else { - if (name is null) ThrowHelper.ArgumentNullException_name(); - if (Properties is null) + for (int i = 0; i < Properties.Count; i++) { - Properties = new (4); - } - else - { - for (int i = 0; i < Properties.Count; i++) + if (Properties[i].Key.Equals(name, StringComparison.Ordinal)) { - if (Properties[i].Key.Equals(name, StringComparison.Ordinal)) - { - return; - } + return; } } - - Properties.Add(new KeyValuePair(name, value is null ? null : Convert.ToString(value, CultureInfo.InvariantCulture))); } - /// - /// Copies/merge the values from this instance to the specified instance. - /// - /// The HTML attributes. - /// If set to true it will merge properties to the target htmlAttributes. Default is false - /// If set to true it will try to share Classes and Properties if destination don't have them, otherwise it will make a copy. Default is true - /// - public void CopyTo(HtmlAttributes htmlAttributes, bool mergeIdAndProperties = false, bool shared = true) + Properties.Add(new KeyValuePair(name, value is null ? null : Convert.ToString(value, CultureInfo.InvariantCulture))); + } + + /// + /// Copies/merge the values from this instance to the specified instance. + /// + /// The HTML attributes. + /// If set to true it will merge properties to the target htmlAttributes. Default is false + /// If set to true it will try to share Classes and Properties if destination don't have them, otherwise it will make a copy. Default is true + /// + public void CopyTo(HtmlAttributes htmlAttributes, bool mergeIdAndProperties = false, bool shared = true) + { + if (htmlAttributes is null) ThrowHelper.ArgumentNullException(nameof(htmlAttributes)); + // Add html htmlAttributes to the object + if (!mergeIdAndProperties || Id != null) { - if (htmlAttributes is null) ThrowHelper.ArgumentNullException(nameof(htmlAttributes)); - // Add html htmlAttributes to the object - if (!mergeIdAndProperties || Id != null) - { - htmlAttributes.Id = Id; - } - if (htmlAttributes.Classes is null) - { - htmlAttributes.Classes = shared ? Classes : Classes != null ? new (Classes) : null; - } - else if (Classes != null) - { - htmlAttributes.Classes.AddRange(Classes); - } + htmlAttributes.Id = Id; + } + if (htmlAttributes.Classes is null) + { + htmlAttributes.Classes = shared ? Classes : Classes != null ? new (Classes) : null; + } + else if (Classes != null) + { + htmlAttributes.Classes.AddRange(Classes); + } - if (htmlAttributes.Properties is null) - { - htmlAttributes.Properties = shared ? Properties : Properties != null ? new (Properties) : null; - } - else if (Properties != null) + if (htmlAttributes.Properties is null) + { + htmlAttributes.Properties = shared ? Properties : Properties != null ? new (Properties) : null; + } + else if (Properties != null) + { + if (mergeIdAndProperties) { - if (mergeIdAndProperties) - { - foreach (var prop in Properties) - { - htmlAttributes.AddPropertyIfNotExist(prop.Key, prop.Value); - } - } - else + foreach (var prop in Properties) { - htmlAttributes.Properties.AddRange(Properties); + htmlAttributes.AddPropertyIfNotExist(prop.Key, prop.Value); } } + else + { + htmlAttributes.Properties.AddRange(Properties); + } } } +} + +/// +/// Extensions for a to allow accessing +/// +public static class HtmlAttributesExtensions +{ + private static readonly object Key = typeof (HtmlAttributes); /// - /// Extensions for a to allow accessing + /// Tries the get stored on a . /// - public static class HtmlAttributesExtensions + /// The markdown object. + /// The attached html attributes or null if not found + public static HtmlAttributes? TryGetAttributes(this IMarkdownObject obj) { - private static readonly object Key = typeof (HtmlAttributes); - - /// - /// Tries the get stored on a . - /// - /// The markdown object. - /// The attached html attributes or null if not found - public static HtmlAttributes? TryGetAttributes(this IMarkdownObject obj) - { - return obj.GetData(Key) as HtmlAttributes; - } + return obj.GetData(Key) as HtmlAttributes; + } - /// - /// Gets or creates the stored on a - /// - /// The markdown object. - /// The attached html attributes - public static HtmlAttributes GetAttributes(this IMarkdownObject obj) + /// + /// Gets or creates the stored on a + /// + /// The markdown object. + /// The attached html attributes + public static HtmlAttributes GetAttributes(this IMarkdownObject obj) + { + var attributes = obj.GetData(Key) as HtmlAttributes; + if (attributes is null) { - var attributes = obj.GetData(Key) as HtmlAttributes; - if (attributes is null) - { - attributes = new HtmlAttributes(); - obj.SetAttributes(attributes); - } - return attributes; + attributes = new HtmlAttributes(); + obj.SetAttributes(attributes); } + return attributes; + } - /// - /// Sets to the - /// - /// The markdown object. - /// The attributes to attach. - public static void SetAttributes(this IMarkdownObject obj, HtmlAttributes attributes) - { - obj.SetData(Key, attributes); - } + /// + /// Sets to the + /// + /// The markdown object. + /// The attributes to attach. + public static void SetAttributes(this IMarkdownObject obj, HtmlAttributes attributes) + { + obj.SetData(Key, attributes); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Html/HtmlBlockRenderer.cs b/src/Markdig/Renderers/Html/HtmlBlockRenderer.cs index 1c2eb39d2..7ce3b0e43 100644 --- a/src/Markdig/Renderers/Html/HtmlBlockRenderer.cs +++ b/src/Markdig/Renderers/Html/HtmlBlockRenderer.cs @@ -4,17 +4,16 @@ using Markdig.Syntax; -namespace Markdig.Renderers.Html +namespace Markdig.Renderers.Html; + +/// +/// A HTML renderer for a . +/// +/// +public class HtmlBlockRenderer : HtmlObjectRenderer { - /// - /// A HTML renderer for a . - /// - /// - public class HtmlBlockRenderer : HtmlObjectRenderer + protected override void Write(HtmlRenderer renderer, HtmlBlock obj) { - protected override void Write(HtmlRenderer renderer, HtmlBlock obj) - { - renderer.WriteLeafRawLines(obj, true, false); - } + renderer.WriteLeafRawLines(obj, true, false); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Html/HtmlObjectRenderer.cs b/src/Markdig/Renderers/Html/HtmlObjectRenderer.cs index db2b159bb..94c410b0f 100644 --- a/src/Markdig/Renderers/Html/HtmlObjectRenderer.cs +++ b/src/Markdig/Renderers/Html/HtmlObjectRenderer.cs @@ -4,14 +4,13 @@ using Markdig.Syntax; -namespace Markdig.Renderers.Html +namespace Markdig.Renderers.Html; + +/// +/// A base class for HTML rendering and Markdown objects. +/// +/// The type of the object. +/// +public abstract class HtmlObjectRenderer : MarkdownObjectRenderer where TObject : MarkdownObject { - /// - /// A base class for HTML rendering and Markdown objects. - /// - /// The type of the object. - /// - public abstract class HtmlObjectRenderer : MarkdownObjectRenderer where TObject : MarkdownObject - { - } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Html/Inlines/AutolinkInlineRenderer.cs b/src/Markdig/Renderers/Html/Inlines/AutolinkInlineRenderer.cs index 817d4842f..793767648 100644 --- a/src/Markdig/Renderers/Html/Inlines/AutolinkInlineRenderer.cs +++ b/src/Markdig/Renderers/Html/Inlines/AutolinkInlineRenderer.cs @@ -3,78 +3,76 @@ // See the license.txt file in the project root for more information. using Markdig.Syntax.Inlines; -using System; -namespace Markdig.Renderers.Html.Inlines +namespace Markdig.Renderers.Html.Inlines; + +/// +/// A HTML renderer for an . +/// +/// +public class AutolinkInlineRenderer : HtmlObjectRenderer { /// - /// A HTML renderer for an . + /// Gets or sets a value indicating whether to always add rel="nofollow" for links or not. /// - /// - public class AutolinkInlineRenderer : HtmlObjectRenderer + [Obsolete("AutoRelNoFollow is obsolete. Please write \"nofollow\" into Property Rel.")] + public bool AutoRelNoFollow { - /// - /// Gets or sets a value indicating whether to always add rel="nofollow" for links or not. - /// - [Obsolete("AutoRelNoFollow is obsolete. Please write \"nofollow\" into Property Rel.")] - public bool AutoRelNoFollow + get { - get - { - return Rel is not null && Rel.Contains("nofollow"); - } - set - { - const string NoFollow = "nofollow"; + return Rel is not null && Rel.Contains("nofollow"); + } + set + { + const string NoFollow = "nofollow"; - if (value) + if (value) + { + if (string.IsNullOrEmpty(Rel)) { - if (string.IsNullOrEmpty(Rel)) - { - Rel = NoFollow; - } - else if (!Rel!.Contains(NoFollow)) - { - Rel += $" {NoFollow}"; - } + Rel = NoFollow; } - else + else if (!Rel!.Contains(NoFollow)) { - Rel = Rel?.Replace(NoFollow, string.Empty); + Rel += $" {NoFollow}"; } } + else + { + Rel = Rel?.Replace(NoFollow, string.Empty); + } } + } - /// - /// Gets or sets the literal string in property rel for links - /// - public string? Rel { get; set; } + /// + /// Gets or sets the literal string in property rel for links + /// + public string? Rel { get; set; } - protected override void Write(HtmlRenderer renderer, AutolinkInline obj) + protected override void Write(HtmlRenderer renderer, AutolinkInline obj) + { + if (renderer.EnableHtmlForInline) { - if (renderer.EnableHtmlForInline) + renderer.Write(obj.IsEmail ? "'); } - renderer.WriteEscape(obj.Url); + renderer.WriteRaw('>'); + } - if (renderer.EnableHtmlForInline) - { - renderer.WriteRaw(""); - } + renderer.WriteEscape(obj.Url); + + if (renderer.EnableHtmlForInline) + { + renderer.WriteRaw(""); } } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Html/Inlines/CodeInlineRenderer.cs b/src/Markdig/Renderers/Html/Inlines/CodeInlineRenderer.cs index d4d1dc5c3..d7e3e004f 100644 --- a/src/Markdig/Renderers/Html/Inlines/CodeInlineRenderer.cs +++ b/src/Markdig/Renderers/Html/Inlines/CodeInlineRenderer.cs @@ -4,34 +4,33 @@ using Markdig.Syntax.Inlines; -namespace Markdig.Renderers.Html.Inlines +namespace Markdig.Renderers.Html.Inlines; + +/// +/// A HTML renderer for a . +/// +/// +public class CodeInlineRenderer : HtmlObjectRenderer { - /// - /// A HTML renderer for a . - /// - /// - public class CodeInlineRenderer : HtmlObjectRenderer + protected override void Write(HtmlRenderer renderer, CodeInline obj) { - protected override void Write(HtmlRenderer renderer, CodeInline obj) + if (renderer.EnableHtmlForInline) + { + renderer.Write("'); + } + if (renderer.EnableHtmlEscape) + { + renderer.WriteEscape(obj.ContentSpan); + } + else + { + renderer.Write(obj.ContentSpan); + } + if (renderer.EnableHtmlForInline) { - if (renderer.EnableHtmlForInline) - { - renderer.Write("'); - } - if (renderer.EnableHtmlEscape) - { - renderer.WriteEscape(obj.ContentSpan); - } - else - { - renderer.Write(obj.ContentSpan); - } - if (renderer.EnableHtmlForInline) - { - renderer.WriteRaw(""); - } + renderer.WriteRaw(""); } } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Html/Inlines/DelimiterInlineRenderer.cs b/src/Markdig/Renderers/Html/Inlines/DelimiterInlineRenderer.cs index 47d89654b..477403e69 100644 --- a/src/Markdig/Renderers/Html/Inlines/DelimiterInlineRenderer.cs +++ b/src/Markdig/Renderers/Html/Inlines/DelimiterInlineRenderer.cs @@ -4,18 +4,17 @@ using Markdig.Syntax.Inlines; -namespace Markdig.Renderers.Html.Inlines +namespace Markdig.Renderers.Html.Inlines; + +/// +/// A HTML renderer for a . +/// +/// +public class DelimiterInlineRenderer : HtmlObjectRenderer { - /// - /// A HTML renderer for a . - /// - /// - public class DelimiterInlineRenderer : HtmlObjectRenderer + protected override void Write(HtmlRenderer renderer, DelimiterInline obj) { - protected override void Write(HtmlRenderer renderer, DelimiterInline obj) - { - renderer.WriteEscape(obj.ToLiteral()); - renderer.WriteChildren(obj); - } + renderer.WriteEscape(obj.ToLiteral()); + renderer.WriteChildren(obj); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Html/Inlines/EmphasisInlineRenderer.cs b/src/Markdig/Renderers/Html/Inlines/EmphasisInlineRenderer.cs index 50c1e9dfc..3a9dc992c 100644 --- a/src/Markdig/Renderers/Html/Inlines/EmphasisInlineRenderer.cs +++ b/src/Markdig/Renderers/Html/Inlines/EmphasisInlineRenderer.cs @@ -5,67 +5,66 @@ using Markdig.Syntax.Inlines; using System.Diagnostics; -namespace Markdig.Renderers.Html.Inlines +namespace Markdig.Renderers.Html.Inlines; + +/// +/// A HTML renderer for an . +/// +/// +public class EmphasisInlineRenderer : HtmlObjectRenderer { /// - /// A HTML renderer for an . + /// Delegates to get the tag associated to an object. + /// + /// The object. + /// The HTML tag associated to this object + public delegate string? GetTagDelegate(EmphasisInline obj); + + /// + /// Initializes a new instance of the class. /// - /// - public class EmphasisInlineRenderer : HtmlObjectRenderer + public EmphasisInlineRenderer() { - /// - /// Delegates to get the tag associated to an object. - /// - /// The object. - /// The HTML tag associated to this object - public delegate string? GetTagDelegate(EmphasisInline obj); + GetTag = GetDefaultTag; + } + + /// + /// Gets or sets the GetTag delegate. + /// + public GetTagDelegate GetTag { get; set; } - /// - /// Initializes a new instance of the class. - /// - public EmphasisInlineRenderer() + protected override void Write(HtmlRenderer renderer, EmphasisInline obj) + { + string? tag = null; + if (renderer.EnableHtmlForInline) { - GetTag = GetDefaultTag; + tag = GetTag(obj); + renderer.Write('<'); + renderer.WriteRaw(tag); + renderer.WriteAttributes(obj); + renderer.WriteRaw('>'); } - - /// - /// Gets or sets the GetTag delegate. - /// - public GetTagDelegate GetTag { get; set; } - - protected override void Write(HtmlRenderer renderer, EmphasisInline obj) + renderer.WriteChildren(obj); + if (renderer.EnableHtmlForInline) { - string? tag = null; - if (renderer.EnableHtmlForInline) - { - tag = GetTag(obj); - renderer.Write('<'); - renderer.WriteRaw(tag); - renderer.WriteAttributes(obj); - renderer.WriteRaw('>'); - } - renderer.WriteChildren(obj); - if (renderer.EnableHtmlForInline) - { - renderer.Write("'); - } + renderer.Write("'); } + } - /// - /// Gets the default HTML tag for ** and __ emphasis. - /// - /// The object. - /// - public static string? GetDefaultTag(EmphasisInline obj) + /// + /// Gets the default HTML tag for ** and __ emphasis. + /// + /// The object. + /// + public static string? GetDefaultTag(EmphasisInline obj) + { + if (obj.DelimiterChar is '*' or '_') { - if (obj.DelimiterChar is '*' or '_') - { - Debug.Assert(obj.DelimiterCount <= 2); - return obj.DelimiterCount == 2 ? "strong" : "em"; - } - return null; + Debug.Assert(obj.DelimiterCount <= 2); + return obj.DelimiterCount == 2 ? "strong" : "em"; } + return null; } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Html/Inlines/HtmlEntityInlineRenderer.cs b/src/Markdig/Renderers/Html/Inlines/HtmlEntityInlineRenderer.cs index 75f896efa..4f31bbbe6 100644 --- a/src/Markdig/Renderers/Html/Inlines/HtmlEntityInlineRenderer.cs +++ b/src/Markdig/Renderers/Html/Inlines/HtmlEntityInlineRenderer.cs @@ -4,24 +4,23 @@ using Markdig.Syntax.Inlines; -namespace Markdig.Renderers.Html.Inlines +namespace Markdig.Renderers.Html.Inlines; + +/// +/// A HTML renderer for a . +/// +/// +public class HtmlEntityInlineRenderer : HtmlObjectRenderer { - /// - /// A HTML renderer for a . - /// - /// - public class HtmlEntityInlineRenderer : HtmlObjectRenderer + protected override void Write(HtmlRenderer renderer, HtmlEntityInline obj) { - protected override void Write(HtmlRenderer renderer, HtmlEntityInline obj) + if (renderer.EnableHtmlEscape) + { + renderer.WriteEscape(obj.Transcoded); + } + else { - if (renderer.EnableHtmlEscape) - { - renderer.WriteEscape(obj.Transcoded); - } - else - { - renderer.Write(obj.Transcoded); - } + renderer.Write(obj.Transcoded); } } } diff --git a/src/Markdig/Renderers/Html/Inlines/HtmlInlineRenderer.cs b/src/Markdig/Renderers/Html/Inlines/HtmlInlineRenderer.cs index 55794d507..d0be60501 100644 --- a/src/Markdig/Renderers/Html/Inlines/HtmlInlineRenderer.cs +++ b/src/Markdig/Renderers/Html/Inlines/HtmlInlineRenderer.cs @@ -4,20 +4,19 @@ using Markdig.Syntax.Inlines; -namespace Markdig.Renderers.Html.Inlines +namespace Markdig.Renderers.Html.Inlines; + +/// +/// A HTML renderer for a . +/// +/// +public class HtmlInlineRenderer : HtmlObjectRenderer { - /// - /// A HTML renderer for a . - /// - /// - public class HtmlInlineRenderer : HtmlObjectRenderer + protected override void Write(HtmlRenderer renderer, HtmlInline obj) { - protected override void Write(HtmlRenderer renderer, HtmlInline obj) + if (renderer.EnableHtmlForInline) { - if (renderer.EnableHtmlForInline) - { - renderer.Write(obj.Tag); - } + renderer.Write(obj.Tag); } } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Html/Inlines/LineBreakInlineRenderer.cs b/src/Markdig/Renderers/Html/Inlines/LineBreakInlineRenderer.cs index 31863a97e..d3cb3e587 100644 --- a/src/Markdig/Renderers/Html/Inlines/LineBreakInlineRenderer.cs +++ b/src/Markdig/Renderers/Html/Inlines/LineBreakInlineRenderer.cs @@ -4,32 +4,31 @@ using Markdig.Syntax.Inlines; -namespace Markdig.Renderers.Html.Inlines +namespace Markdig.Renderers.Html.Inlines; + +/// +/// A HTML renderer for a . +/// +/// +public class LineBreakInlineRenderer : HtmlObjectRenderer { /// - /// A HTML renderer for a . + /// Gets or sets a value indicating whether to render this softline break as a HTML hardline break tag (<br />) /// - /// - public class LineBreakInlineRenderer : HtmlObjectRenderer + public bool RenderAsHardlineBreak { get; set; } + + protected override void Write(HtmlRenderer renderer, LineBreakInline obj) { - /// - /// Gets or sets a value indicating whether to render this softline break as a HTML hardline break tag (<br />) - /// - public bool RenderAsHardlineBreak { get; set; } + if (renderer.IsLastInContainer) return; - protected override void Write(HtmlRenderer renderer, LineBreakInline obj) + if (renderer.EnableHtmlForInline) { - if (renderer.IsLastInContainer) return; - - if (renderer.EnableHtmlForInline) + if (obj.IsHard || RenderAsHardlineBreak) { - if (obj.IsHard || RenderAsHardlineBreak) - { - renderer.WriteLine("
    "); - } + renderer.WriteLine("
    "); } - - renderer.EnsureLine(); } + + renderer.EnsureLine(); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Html/Inlines/LinkInlineRenderer.cs b/src/Markdig/Renderers/Html/Inlines/LinkInlineRenderer.cs index 7b7e06fc5..4721225a6 100644 --- a/src/Markdig/Renderers/Html/Inlines/LinkInlineRenderer.cs +++ b/src/Markdig/Renderers/Html/Inlines/LinkInlineRenderer.cs @@ -3,104 +3,102 @@ // See the license.txt file in the project root for more information. using Markdig.Syntax.Inlines; -using System; -namespace Markdig.Renderers.Html.Inlines +namespace Markdig.Renderers.Html.Inlines; + +/// +/// A HTML renderer for a . +/// +/// +public class LinkInlineRenderer : HtmlObjectRenderer { /// - /// A HTML renderer for a . + /// Gets or sets a value indicating whether to always add rel="nofollow" for links or not. /// - /// - public class LinkInlineRenderer : HtmlObjectRenderer + [Obsolete("AutoRelNoFollow is obsolete. Please write \"nofollow\" into Property Rel.")] + public bool AutoRelNoFollow { - /// - /// Gets or sets a value indicating whether to always add rel="nofollow" for links or not. - /// - [Obsolete("AutoRelNoFollow is obsolete. Please write \"nofollow\" into Property Rel.")] - public bool AutoRelNoFollow + get + { + return Rel is not null && Rel.Contains("nofollow"); + } + set { - get + const string rel = "nofollow"; + if (value) { - return Rel is not null && Rel.Contains("nofollow"); + if (string.IsNullOrEmpty(Rel)) + Rel = rel; + else if (!Rel!.Contains(rel)) + Rel += $" {rel}"; } - set + else if (!value && Rel is not null) { - const string rel = "nofollow"; - if (value) - { - if (string.IsNullOrEmpty(Rel)) - Rel = rel; - else if (!Rel!.Contains(rel)) - Rel += $" {rel}"; - } - else if (!value && Rel is not null) - { - Rel = Rel.Replace(rel, string.Empty); - } + Rel = Rel.Replace(rel, string.Empty); } } + } - /// - /// Gets or sets the literal string in property rel for links - /// - public string? Rel { get; set; } + /// + /// Gets or sets the literal string in property rel for links + /// + public string? Rel { get; set; } - protected override void Write(HtmlRenderer renderer, LinkInline link) + protected override void Write(HtmlRenderer renderer, LinkInline link) + { + if (renderer.EnableHtmlForInline) + { + renderer.Write(link.IsImage ? "\"");"); } - - if (link.IsImage) + } + else + { + if (renderer.EnableHtmlForInline) { - if (renderer.EnableHtmlForInline) + if (!string.IsNullOrWhiteSpace(Rel)) { - renderer.WriteRaw(" />"); + renderer.WriteRaw(" rel=\""); + renderer.WriteRaw(Rel); + renderer.WriteRaw('"'); } + renderer.WriteRaw('>'); } - else + renderer.WriteChildren(link); + if (renderer.EnableHtmlForInline) { - if (renderer.EnableHtmlForInline) - { - if (!string.IsNullOrWhiteSpace(Rel)) - { - renderer.WriteRaw(" rel=\""); - renderer.WriteRaw(Rel); - renderer.WriteRaw('"'); - } - renderer.WriteRaw('>'); - } - renderer.WriteChildren(link); - if (renderer.EnableHtmlForInline) - { - renderer.Write(""); - } + renderer.Write(""); } } } diff --git a/src/Markdig/Renderers/Html/Inlines/LiteralInlineRenderer.cs b/src/Markdig/Renderers/Html/Inlines/LiteralInlineRenderer.cs index 5de6985ed..f9a71f80c 100644 --- a/src/Markdig/Renderers/Html/Inlines/LiteralInlineRenderer.cs +++ b/src/Markdig/Renderers/Html/Inlines/LiteralInlineRenderer.cs @@ -4,24 +4,23 @@ using Markdig.Syntax.Inlines; -namespace Markdig.Renderers.Html.Inlines +namespace Markdig.Renderers.Html.Inlines; + +/// +/// A HTML renderer for a . +/// +/// +public class LiteralInlineRenderer : HtmlObjectRenderer { - /// - /// A HTML renderer for a . - /// - /// - public class LiteralInlineRenderer : HtmlObjectRenderer + protected override void Write(HtmlRenderer renderer, LiteralInline obj) { - protected override void Write(HtmlRenderer renderer, LiteralInline obj) + if (renderer.EnableHtmlEscape) + { + renderer.WriteEscape(ref obj.Content); + } + else { - if (renderer.EnableHtmlEscape) - { - renderer.WriteEscape(ref obj.Content); - } - else - { - renderer.Write(ref obj.Content); - } + renderer.Write(ref obj.Content); } } } diff --git a/src/Markdig/Renderers/Html/ListRenderer.cs b/src/Markdig/Renderers/Html/ListRenderer.cs index 931c0e939..dca55896f 100644 --- a/src/Markdig/Renderers/Html/ListRenderer.cs +++ b/src/Markdig/Renderers/Html/ListRenderer.cs @@ -4,77 +4,76 @@ using Markdig.Syntax; -namespace Markdig.Renderers.Html +namespace Markdig.Renderers.Html; + +/// +/// A HTML renderer for a . +/// +/// +public class ListRenderer : HtmlObjectRenderer { - /// - /// A HTML renderer for a . - /// - /// - public class ListRenderer : HtmlObjectRenderer + protected override void Write(HtmlRenderer renderer, ListBlock listBlock) { - protected override void Write(HtmlRenderer renderer, ListBlock listBlock) + renderer.EnsureLine(); + if (renderer.EnableHtmlForBlock) { - renderer.EnsureLine(); - if (renderer.EnableHtmlForBlock) + if (listBlock.IsOrdered) { - if (listBlock.IsOrdered) + renderer.Write("'); + renderer.WriteRaw(" type=\""); + renderer.WriteRaw(listBlock.BulletType); + renderer.WriteRaw('"'); } - else + + if (listBlock.OrderedStart is not null && listBlock.OrderedStart != "1") { - renderer.Write("'); + renderer.Write(" start=\""); + renderer.WriteRaw(listBlock.OrderedStart); + renderer.WriteRaw('"'); } + renderer.WriteAttributes(listBlock); + renderer.WriteLine('>'); } - - foreach (var item in listBlock) + else { - var listItem = (ListItemBlock)item; - var previousImplicit = renderer.ImplicitParagraph; - renderer.ImplicitParagraph = !listBlock.IsLoose; - - renderer.EnsureLine(); - if (renderer.EnableHtmlForBlock) - { - renderer.Write("'); - } - - renderer.WriteChildren(listItem); + renderer.Write("'); + } + } - if (renderer.EnableHtmlForBlock) - { - renderer.WriteLine(""); - } + foreach (var item in listBlock) + { + var listItem = (ListItemBlock)item; + var previousImplicit = renderer.ImplicitParagraph; + renderer.ImplicitParagraph = !listBlock.IsLoose; - renderer.EnsureLine(); - renderer.ImplicitParagraph = previousImplicit; + renderer.EnsureLine(); + if (renderer.EnableHtmlForBlock) + { + renderer.Write("'); } + renderer.WriteChildren(listItem); + if (renderer.EnableHtmlForBlock) { - renderer.WriteLine(listBlock.IsOrdered ? "
" : ""); + renderer.WriteLine(""); } renderer.EnsureLine(); + renderer.ImplicitParagraph = previousImplicit; } + + if (renderer.EnableHtmlForBlock) + { + renderer.WriteLine(listBlock.IsOrdered ? "" : ""); + } + + renderer.EnsureLine(); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Html/ParagraphRenderer.cs b/src/Markdig/Renderers/Html/ParagraphRenderer.cs index 5630db47b..907bc491b 100644 --- a/src/Markdig/Renderers/Html/ParagraphRenderer.cs +++ b/src/Markdig/Renderers/Html/ParagraphRenderer.cs @@ -4,37 +4,36 @@ using Markdig.Syntax; -namespace Markdig.Renderers.Html +namespace Markdig.Renderers.Html; + +/// +/// A HTML renderer for a . +/// +/// +public class ParagraphRenderer : HtmlObjectRenderer { - /// - /// A HTML renderer for a . - /// - /// - public class ParagraphRenderer : HtmlObjectRenderer + protected override void Write(HtmlRenderer renderer, ParagraphBlock obj) { - protected override void Write(HtmlRenderer renderer, ParagraphBlock obj) + if (!renderer.ImplicitParagraph && renderer.EnableHtmlForBlock) { - if (!renderer.ImplicitParagraph && renderer.EnableHtmlForBlock) + if (!renderer.IsFirstInContainer) { - if (!renderer.IsFirstInContainer) - { - renderer.EnsureLine(); - } - - renderer.Write("'); + renderer.EnsureLine(); } - renderer.WriteLeafInline(obj); - if (!renderer.ImplicitParagraph) - { - if (renderer.EnableHtmlForBlock) - { - renderer.WriteLine("

"); - } - renderer.EnsureLine(); + renderer.Write("'); + } + renderer.WriteLeafInline(obj); + if (!renderer.ImplicitParagraph) + { + if (renderer.EnableHtmlForBlock) + { + renderer.WriteLine("

"); } + + renderer.EnsureLine(); } } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Html/QuoteBlockRenderer.cs b/src/Markdig/Renderers/Html/QuoteBlockRenderer.cs index dbc44c7b9..c1a1cf150 100644 --- a/src/Markdig/Renderers/Html/QuoteBlockRenderer.cs +++ b/src/Markdig/Renderers/Html/QuoteBlockRenderer.cs @@ -4,32 +4,31 @@ using Markdig.Syntax; -namespace Markdig.Renderers.Html +namespace Markdig.Renderers.Html; + +/// +/// A HTML renderer for a . +/// +/// +public class QuoteBlockRenderer : HtmlObjectRenderer { - /// - /// A HTML renderer for a . - /// - /// - public class QuoteBlockRenderer : HtmlObjectRenderer + protected override void Write(HtmlRenderer renderer, QuoteBlock obj) { - protected override void Write(HtmlRenderer renderer, QuoteBlock obj) + renderer.EnsureLine(); + if (renderer.EnableHtmlForBlock) + { + renderer.Write("'); + } + var savedImplicitParagraph = renderer.ImplicitParagraph; + renderer.ImplicitParagraph = false; + renderer.WriteChildren(obj); + renderer.ImplicitParagraph = savedImplicitParagraph; + if (renderer.EnableHtmlForBlock) { - renderer.EnsureLine(); - if (renderer.EnableHtmlForBlock) - { - renderer.Write("'); - } - var savedImplicitParagraph = renderer.ImplicitParagraph; - renderer.ImplicitParagraph = false; - renderer.WriteChildren(obj); - renderer.ImplicitParagraph = savedImplicitParagraph; - if (renderer.EnableHtmlForBlock) - { - renderer.WriteLine(""); - } - renderer.EnsureLine(); + renderer.WriteLine(""); } + renderer.EnsureLine(); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Html/ThematicBreakRenderer.cs b/src/Markdig/Renderers/Html/ThematicBreakRenderer.cs index f824fd463..4b21e0f12 100644 --- a/src/Markdig/Renderers/Html/ThematicBreakRenderer.cs +++ b/src/Markdig/Renderers/Html/ThematicBreakRenderer.cs @@ -4,22 +4,21 @@ using Markdig.Syntax; -namespace Markdig.Renderers.Html +namespace Markdig.Renderers.Html; + +/// +/// A HTML renderer for a . +/// +/// +public class ThematicBreakRenderer : HtmlObjectRenderer { - /// - /// A HTML renderer for a . - /// - /// - public class ThematicBreakRenderer : HtmlObjectRenderer + protected override void Write(HtmlRenderer renderer, ThematicBreakBlock obj) { - protected override void Write(HtmlRenderer renderer, ThematicBreakBlock obj) + if (renderer.EnableHtmlForBlock) { - if (renderer.EnableHtmlForBlock) - { - renderer.Write(""); - } + renderer.Write(""); } } } \ No newline at end of file diff --git a/src/Markdig/Renderers/HtmlRenderer.cs b/src/Markdig/Renderers/HtmlRenderer.cs index 29fbe1b28..1b41b339a 100644 --- a/src/Markdig/Renderers/HtmlRenderer.cs +++ b/src/Markdig/Renderers/HtmlRenderer.cs @@ -2,466 +2,465 @@ // 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.Globalization; using System.IO; using System.Runtime.CompilerServices; using System.Text; + using Markdig.Helpers; using Markdig.Renderers.Html; using Markdig.Renderers.Html.Inlines; using Markdig.Syntax; -namespace Markdig.Renderers +namespace Markdig.Renderers; + +/// +/// Default HTML renderer for a Markdown object. +/// +/// +public class HtmlRenderer : TextRendererBase { + private static readonly char[] s_writeEscapeIndexOfAnyChars = new[] { '<', '>', '&', '"' }; + /// - /// Default HTML renderer for a Markdown object. + /// Initializes a new instance of the class. /// - /// - public class HtmlRenderer : TextRendererBase + /// The writer. + public HtmlRenderer(TextWriter writer) : base(writer) { - private static readonly char[] s_writeEscapeIndexOfAnyChars = new[] { '<', '>', '&', '"' }; + // Default block renderers + ObjectRenderers.Add(new CodeBlockRenderer()); + ObjectRenderers.Add(new ListRenderer()); + ObjectRenderers.Add(new HeadingRenderer()); + ObjectRenderers.Add(new HtmlBlockRenderer()); + ObjectRenderers.Add(new ParagraphRenderer()); + ObjectRenderers.Add(new QuoteBlockRenderer()); + ObjectRenderers.Add(new ThematicBreakRenderer()); + + // Default inline renderers + ObjectRenderers.Add(new AutolinkInlineRenderer()); + ObjectRenderers.Add(new CodeInlineRenderer()); + ObjectRenderers.Add(new DelimiterInlineRenderer()); + ObjectRenderers.Add(new EmphasisInlineRenderer()); + ObjectRenderers.Add(new LineBreakInlineRenderer()); + ObjectRenderers.Add(new HtmlInlineRenderer()); + ObjectRenderers.Add(new HtmlEntityInlineRenderer()); + ObjectRenderers.Add(new LinkInlineRenderer()); + ObjectRenderers.Add(new LiteralInlineRenderer()); + + EnableHtmlForBlock = true; + EnableHtmlForInline = true; + EnableHtmlEscape = true; + } - /// - /// Initializes a new instance of the class. - /// - /// The writer. - public HtmlRenderer(TextWriter writer) : base(writer) - { - // Default block renderers - ObjectRenderers.Add(new CodeBlockRenderer()); - ObjectRenderers.Add(new ListRenderer()); - ObjectRenderers.Add(new HeadingRenderer()); - ObjectRenderers.Add(new HtmlBlockRenderer()); - ObjectRenderers.Add(new ParagraphRenderer()); - ObjectRenderers.Add(new QuoteBlockRenderer()); - ObjectRenderers.Add(new ThematicBreakRenderer()); - - // Default inline renderers - ObjectRenderers.Add(new AutolinkInlineRenderer()); - ObjectRenderers.Add(new CodeInlineRenderer()); - ObjectRenderers.Add(new DelimiterInlineRenderer()); - ObjectRenderers.Add(new EmphasisInlineRenderer()); - ObjectRenderers.Add(new LineBreakInlineRenderer()); - ObjectRenderers.Add(new HtmlInlineRenderer()); - ObjectRenderers.Add(new HtmlEntityInlineRenderer()); - ObjectRenderers.Add(new LinkInlineRenderer()); - ObjectRenderers.Add(new LiteralInlineRenderer()); - - EnableHtmlForBlock = true; - EnableHtmlForInline = true; - EnableHtmlEscape = true; - } + /// + /// Gets or sets a value indicating whether to output HTML tags when rendering. See remarks. + /// + /// + /// This is used by some renderers to disable HTML tags when rendering some inline elements (for image links). + /// + public bool EnableHtmlForInline { get; set; } - /// - /// Gets or sets a value indicating whether to output HTML tags when rendering. See remarks. - /// - /// - /// This is used by some renderers to disable HTML tags when rendering some inline elements (for image links). - /// - public bool EnableHtmlForInline { get; set; } - - /// - /// Gets or sets a value indicating whether to output HTML tags when rendering. See remarks. - /// - /// - /// This is used by some renderers to disable HTML tags when rendering some block elements (for image links). - /// - public bool EnableHtmlForBlock { get; set; } - - public bool EnableHtmlEscape { get; set; } - - /// - /// Gets or sets a value indicating whether to use implicit paragraph (optional <p>) - /// - public bool ImplicitParagraph { get; set; } - - public bool UseNonAsciiNoEscape { get; set; } - - /// - /// Gets a value to use as the base url for all relative links - /// - public Uri? BaseUrl { get; set; } - - /// - /// Allows links to be rewritten - /// - public Func? LinkRewriter { get; set; } - - /// - /// Writes the content escaped for HTML. - /// - /// The content. - /// This instance - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public HtmlRenderer WriteEscape(string? content) - { - WriteEscape(content.AsSpan()); - return this; - } + /// + /// Gets or sets a value indicating whether to output HTML tags when rendering. See remarks. + /// + /// + /// This is used by some renderers to disable HTML tags when rendering some block elements (for image links). + /// + public bool EnableHtmlForBlock { get; set; } - /// - /// Writes the content escaped for HTML. - /// - /// The slice. - /// Only escape < and & - /// This instance - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public HtmlRenderer WriteEscape(ref StringSlice slice, bool softEscape = false) - { - WriteEscape(slice.AsSpan(), softEscape); - return this; - } + public bool EnableHtmlEscape { get; set; } - /// - /// Writes the content escaped for HTML. - /// - /// The slice. - /// Only escape < and & - /// This instance - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public HtmlRenderer WriteEscape(StringSlice slice, bool softEscape = false) - { - WriteEscape(slice.AsSpan(), softEscape); - return this; - } + /// + /// Gets or sets a value indicating whether to use implicit paragraph (optional <p>) + /// + public bool ImplicitParagraph { get; set; } - /// - /// Writes the content escaped for HTML. - /// - /// The content. - /// The offset. - /// The length. - /// Only escape < and & - /// This instance - public HtmlRenderer WriteEscape(string content, int offset, int length, bool softEscape = false) - { - WriteEscape(content.AsSpan(offset, length), softEscape); - return this; - } + public bool UseNonAsciiNoEscape { get; set; } + + /// + /// Gets a value to use as the base url for all relative links + /// + public Uri? BaseUrl { get; set; } + + /// + /// Allows links to be rewritten + /// + public Func? LinkRewriter { get; set; } + + /// + /// Writes the content escaped for HTML. + /// + /// The content. + /// This instance + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HtmlRenderer WriteEscape(string? content) + { + WriteEscape(content.AsSpan()); + return this; + } + + /// + /// Writes the content escaped for HTML. + /// + /// The slice. + /// Only escape < and & + /// This instance + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HtmlRenderer WriteEscape(ref StringSlice slice, bool softEscape = false) + { + WriteEscape(slice.AsSpan(), softEscape); + return this; + } - /// - /// Writes the content escaped for HTML. - /// - /// The content. - /// Only escape < and & - public void WriteEscape(ReadOnlySpan content, bool softEscape = false) + /// + /// Writes the content escaped for HTML. + /// + /// The slice. + /// Only escape < and & + /// This instance + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HtmlRenderer WriteEscape(StringSlice slice, bool softEscape = false) + { + WriteEscape(slice.AsSpan(), softEscape); + return this; + } + + /// + /// Writes the content escaped for HTML. + /// + /// The content. + /// The offset. + /// The length. + /// Only escape < and & + /// This instance + public HtmlRenderer WriteEscape(string content, int offset, int length, bool softEscape = false) + { + WriteEscape(content.AsSpan(offset, length), softEscape); + return this; + } + + /// + /// Writes the content escaped for HTML. + /// + /// The content. + /// Only escape < and & + public void WriteEscape(ReadOnlySpan content, bool softEscape = false) + { + if (!content.IsEmpty) { - if (!content.IsEmpty) + int nextIndex = content.IndexOfAny(s_writeEscapeIndexOfAnyChars); + if (nextIndex == -1) { - int nextIndex = content.IndexOfAny(s_writeEscapeIndexOfAnyChars); - if (nextIndex == -1) - { - Write(content); - } - else - { - WriteEscapeSlow(content, softEscape); - } + Write(content); + } + else + { + WriteEscapeSlow(content, softEscape); } } + } - private void WriteEscapeSlow(ReadOnlySpan content, bool softEscape = false) - { - WriteIndent(); + private void WriteEscapeSlow(ReadOnlySpan content, bool softEscape = false) + { + WriteIndent(); - int previousOffset = 0; - for (int i = 0; i < content.Length; i++) + int previousOffset = 0; + for (int i = 0; i < content.Length; i++) + { + switch (content[i]) { - switch (content[i]) - { - case '<': + case '<': + WriteRaw(content.Slice(previousOffset, i - previousOffset)); + if (EnableHtmlEscape) + { + WriteRaw("<"); + } + previousOffset = i + 1; + break; + case '>': + if (!softEscape) + { WriteRaw(content.Slice(previousOffset, i - previousOffset)); if (EnableHtmlEscape) { - WriteRaw("<"); + WriteRaw(">"); } previousOffset = i + 1; - break; - case '>': - if (!softEscape) - { - WriteRaw(content.Slice(previousOffset, i - previousOffset)); - if (EnableHtmlEscape) - { - WriteRaw(">"); - } - previousOffset = i + 1; - } - break; - case '&': + } + break; + case '&': + WriteRaw(content.Slice(previousOffset, i - previousOffset)); + if (EnableHtmlEscape) + { + WriteRaw("&"); + } + previousOffset = i + 1; + break; + case '"': + if (!softEscape) + { WriteRaw(content.Slice(previousOffset, i - previousOffset)); if (EnableHtmlEscape) { - WriteRaw("&"); + WriteRaw("""); } previousOffset = i + 1; - break; - case '"': - if (!softEscape) - { - WriteRaw(content.Slice(previousOffset, i - previousOffset)); - if (EnableHtmlEscape) - { - WriteRaw("""); - } - previousOffset = i + 1; - } - break; - } + } + break; } - - WriteRaw(content.Slice(previousOffset)); } - private static readonly IdnMapping IdnMapping = new IdnMapping(); + WriteRaw(content.Slice(previousOffset)); + } + + private static readonly IdnMapping IdnMapping = new IdnMapping(); - /// - /// Writes the URL escaped for HTML. - /// - /// The content. - /// This instance - public HtmlRenderer WriteEscapeUrl(string? content) + /// + /// Writes the URL escaped for HTML. + /// + /// The content. + /// This instance + public HtmlRenderer WriteEscapeUrl(string? content) + { + if (content is null) + return this; + + if (BaseUrl != null + // According to https://github.com/dotnet/runtime/issues/22718 + // this is the proper cross-platform way to check whether a uri is absolute or not: + && Uri.TryCreate(content, UriKind.RelativeOrAbsolute, out var contentUri) && !contentUri.IsAbsoluteUri) { - if (content is null) - return this; + content = new Uri(BaseUrl, contentUri).AbsoluteUri; + } - if (BaseUrl != null - // According to https://github.com/dotnet/runtime/issues/22718 - // this is the proper cross-platform way to check whether a uri is absolute or not: - && Uri.TryCreate(content, UriKind.RelativeOrAbsolute, out var contentUri) && !contentUri.IsAbsoluteUri) - { - content = new Uri(BaseUrl, contentUri).AbsoluteUri; - } + if (LinkRewriter != null) + { + content = LinkRewriter(content); + } - if (LinkRewriter != null) + // 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 :// + WriteEscapeUrl(content, 0, schemeOffset); + + bool idnaEncodeDomain = false; + int endOfDomain = schemeOffset; + for (; endOfDomain < content.Length; endOfDomain++) { - content = LinkRewriter(content); + char c = content[endOfDomain]; + if (c == '/' || c == '?' || c == '#' || c == ':') // End of domain part + { + break; + } + if (c > 127) + { + idnaEncodeDomain = true; + } } - // a://c.d = 7 chars - int schemeOffset = content.Length < 7 ? -1 : content.IndexOf("://", StringComparison.Ordinal); - if (schemeOffset != -1) // This is an absolute URL + if (idnaEncodeDomain) { - schemeOffset += 3; // skip :// - WriteEscapeUrl(content, 0, schemeOffset); + string domainName; - bool idnaEncodeDomain = false; - int endOfDomain = schemeOffset; - for (; endOfDomain < content.Length; endOfDomain++) + try { - char c = content[endOfDomain]; - if (c == '/' || c == '?' || c == '#' || c == ':') // End of domain part - { - break; - } - if (c > 127) - { - idnaEncodeDomain = true; - } + domainName = IdnMapping.GetAscii(content, schemeOffset, endOfDomain - schemeOffset); } - - if (idnaEncodeDomain) + catch { - string domainName; - - try - { - domainName = IdnMapping.GetAscii(content, schemeOffset, endOfDomain - schemeOffset); - } - catch - { - // Not a valid IDN, fallback to non-punycode encoding - WriteEscapeUrl(content, schemeOffset, content.Length); - return this; - } + // Not a valid IDN, fallback to non-punycode encoding + WriteEscapeUrl(content, schemeOffset, content.Length); + return this; + } - // Escape the characters (see Commonmark example 327 and think of it with a non-ascii symbol) - int previousPosition = 0; - for (int i = 0; i < domainName.Length; i++) + // Escape the characters (see Commonmark example 327 and think of it with a non-ascii symbol) + int previousPosition = 0; + for (int i = 0; i < domainName.Length; i++) + { + var escape = HtmlHelper.EscapeUrlCharacter(domainName[i]); + if (escape != null) { - var escape = HtmlHelper.EscapeUrlCharacter(domainName[i]); - if (escape != null) - { - Write(domainName, previousPosition, i - previousPosition); - previousPosition = i + 1; - Write(escape); - } + Write(domainName, previousPosition, i - previousPosition); + previousPosition = i + 1; + Write(escape); } - Write(domainName, previousPosition, domainName.Length - previousPosition); - WriteEscapeUrl(content, endOfDomain, content.Length); - } - else - { - WriteEscapeUrl(content, schemeOffset, content.Length); } + Write(domainName, previousPosition, domainName.Length - previousPosition); + WriteEscapeUrl(content, endOfDomain, content.Length); } - else // This is a relative URL + else { - WriteEscapeUrl(content, 0, content.Length); + WriteEscapeUrl(content, schemeOffset, content.Length); } - - return this; + } + else // This is a relative URL + { + WriteEscapeUrl(content, 0, content.Length); } - private void WriteEscapeUrl(string content, int start, int length) + return this; + } + + private void WriteEscapeUrl(string content, int start, int length) + { + int previousPosition = start; + for (var i = previousPosition; i < length; i++) { - int previousPosition = start; - for (var i = previousPosition; i < length; i++) + var c = content[i]; + + if (c < 128) + { + var escape = HtmlHelper.EscapeUrlCharacter(c); + if (escape != null) + { + Write(content, previousPosition, i - previousPosition); + previousPosition = i + 1; + Write(escape); + } + } + else { - var c = content[i]; + Write(content, previousPosition, i - previousPosition); + previousPosition = i + 1; - if (c < 128) + // Special case for Edge/IE workaround for MarkdownEditor, don't escape non-ASCII chars to make image links working + if (UseNonAsciiNoEscape) { - var escape = HtmlHelper.EscapeUrlCharacter(c); - if (escape != null) - { - Write(content, previousPosition, i - previousPosition); - previousPosition = i + 1; - Write(escape); - } + Write(c); } else { - Write(content, previousPosition, i - previousPosition); - previousPosition = i + 1; - - // Special case for Edge/IE workaround for MarkdownEditor, don't escape non-ASCII chars to make image links working - if (UseNonAsciiNoEscape) + byte[] bytes; + if (c >= '\ud800' && c <= '\udfff' && previousPosition < length) { - Write(c); + bytes = Encoding.UTF8.GetBytes(new[] { c, content[previousPosition] }); + // Skip next char as it is decoded above + i++; + previousPosition = i + 1; } else { - byte[] bytes; - if (c >= '\ud800' && c <= '\udfff' && previousPosition < length) - { - bytes = Encoding.UTF8.GetBytes(new[] { c, content[previousPosition] }); - // Skip next char as it is decoded above - i++; - previousPosition = i + 1; - } - else - { - bytes = Encoding.UTF8.GetBytes(new[] { c }); - } - for (var j = 0; j < bytes.Length; j++) - { - Write($"%{bytes[j]:X2}"); - } + bytes = Encoding.UTF8.GetBytes(new[] { c }); + } + for (var j = 0; j < bytes.Length; j++) + { + Write($"%{bytes[j]:X2}"); } } } - Write(content, previousPosition, length - previousPosition); + } + Write(content, previousPosition, length - previousPosition); + } + + /// + /// Writes the attached on the specified . + /// + /// The object. + /// + public HtmlRenderer WriteAttributes(MarkdownObject markdownObject) + { + if (markdownObject is null) ThrowHelper.ArgumentNullException_markdownObject(); + return WriteAttributes(markdownObject.TryGetAttributes()); + } + + /// + /// Writes the specified . + /// + /// The attributes to render. + /// A class filter used to transform a class into another class at writing time + /// This instance + public HtmlRenderer WriteAttributes(HtmlAttributes? attributes, Func? classFilter = null) + { + if (attributes is null) + { + return this; } - /// - /// Writes the attached on the specified . - /// - /// The object. - /// - public HtmlRenderer WriteAttributes(MarkdownObject markdownObject) + if (attributes.Id != null) { - if (markdownObject is null) ThrowHelper.ArgumentNullException_markdownObject(); - return WriteAttributes(markdownObject.TryGetAttributes()); + Write(" id=\""); + WriteEscape(attributes.Id); + WriteRaw('"'); } - /// - /// Writes the specified . - /// - /// The attributes to render. - /// A class filter used to transform a class into another class at writing time - /// This instance - public HtmlRenderer WriteAttributes(HtmlAttributes? attributes, Func? classFilter = null) + if (attributes.Classes is { Count: > 0 }) { - if (attributes is null) + Write(" class=\""); + for (int i = 0; i < attributes.Classes.Count; i++) { - return this; + var cssClass = attributes.Classes[i]; + if (i > 0) + { + WriteRaw(' '); + } + WriteEscape(classFilter != null ? classFilter(cssClass) : cssClass); } + WriteRaw('"'); + } - if (attributes.Id != null) + if (attributes.Properties is { Count: > 0 }) + { + foreach (var property in attributes.Properties) { - Write(" id=\""); - WriteEscape(attributes.Id); + Write(' '); + WriteRaw(property.Key); + WriteRaw("=\""); + WriteEscape(property.Value ?? ""); WriteRaw('"'); } + } + + return this; + } - if (attributes.Classes is { Count: > 0 }) + /// + /// Writes the lines of a + /// + /// The leaf block. + /// if set to true write end of lines. + /// if set to true escape the content for HTML + /// Only escape < and & + /// This instance + public HtmlRenderer WriteLeafRawLines(LeafBlock leafBlock, bool writeEndOfLines, bool escape, bool softEscape = false) + { + if (leafBlock is null) ThrowHelper.ArgumentNullException_leafBlock(); + + var slices = leafBlock.Lines.Lines; + if (slices is not null) + { + for (int i = 0; i < slices.Length; i++) { - Write(" class=\""); - for (int i = 0; i < attributes.Classes.Count; i++) + ref StringSlice slice = ref slices[i].Slice; + if (slice.Text is null) { - var cssClass = attributes.Classes[i]; - if (i > 0) - { - WriteRaw(' '); - } - WriteEscape(classFilter != null ? classFilter(cssClass) : cssClass); + break; } - WriteRaw('"'); - } - if (attributes.Properties is { Count: > 0 }) - { - foreach (var property in attributes.Properties) + if (!writeEndOfLines && i > 0) { - Write(' '); - WriteRaw(property.Key); - WriteRaw("=\""); - WriteEscape(property.Value ?? ""); - WriteRaw('"'); + WriteLine(); } - } - - return this; - } - - /// - /// Writes the lines of a - /// - /// The leaf block. - /// if set to true write end of lines. - /// if set to true escape the content for HTML - /// Only escape < and & - /// This instance - public HtmlRenderer WriteLeafRawLines(LeafBlock leafBlock, bool writeEndOfLines, bool escape, bool softEscape = false) - { - if (leafBlock is null) ThrowHelper.ArgumentNullException_leafBlock(); - var slices = leafBlock.Lines.Lines; - if (slices is not null) - { - for (int i = 0; i < slices.Length; i++) + ReadOnlySpan span = slice.AsSpan(); + if (escape) { - ref StringSlice slice = ref slices[i].Slice; - if (slice.Text is null) - { - break; - } - - if (!writeEndOfLines && i > 0) - { - WriteLine(); - } - - ReadOnlySpan span = slice.AsSpan(); - if (escape) - { - WriteEscape(span, softEscape); - } - else - { - Write(span); - } + WriteEscape(span, softEscape); + } + else + { + Write(span); + } - if (writeEndOfLines) - { - WriteLine(); - } + if (writeEndOfLines) + { + WriteLine(); } } - - return this; } + + return this; } } \ No newline at end of file diff --git a/src/Markdig/Renderers/IMarkdownObjectRenderer.cs b/src/Markdig/Renderers/IMarkdownObjectRenderer.cs index 8ecd1a356..c656f7288 100644 --- a/src/Markdig/Renderers/IMarkdownObjectRenderer.cs +++ b/src/Markdig/Renderers/IMarkdownObjectRenderer.cs @@ -3,28 +3,26 @@ // See the license.txt file in the project root for more information. using Markdig.Syntax; -using System; -namespace Markdig.Renderers +namespace Markdig.Renderers; + +/// +/// Base interface for the renderer of a . +/// +public interface IMarkdownObjectRenderer { /// - /// Base interface for the renderer of a . + /// Accepts the specified . /// - public interface IMarkdownObjectRenderer - { - /// - /// Accepts the specified . - /// - /// The renderer. - /// The of the Markdown object. - /// true If this renderer is accepting to render the specified Markdown object - bool Accept(RendererBase renderer, Type objectType); + /// The renderer. + /// The of the Markdown object. + /// true If this renderer is accepting to render the specified Markdown object + bool Accept(RendererBase renderer, Type objectType); - /// - /// Writes the specified to the . - /// - /// The renderer. - /// The object to render. - void Write(RendererBase renderer, MarkdownObject objectToRender); - } + /// + /// Writes the specified to the . + /// + /// The renderer. + /// The object to render. + void Write(RendererBase renderer, MarkdownObject objectToRender); } \ No newline at end of file diff --git a/src/Markdig/Renderers/IMarkdownRenderer.cs b/src/Markdig/Renderers/IMarkdownRenderer.cs index 9ef00776a..585f41322 100644 --- a/src/Markdig/Renderers/IMarkdownRenderer.cs +++ b/src/Markdig/Renderers/IMarkdownRenderer.cs @@ -2,37 +2,35 @@ // 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 Markdig.Syntax; using Markdig.Syntax.Inlines; -namespace Markdig.Renderers +namespace Markdig.Renderers; + +/// +/// Base interface for a renderer for a Markdown . +/// +public interface IMarkdownRenderer { /// - /// Base interface for a renderer for a Markdown . + /// Occurs when before writing an object. /// - public interface IMarkdownRenderer - { - /// - /// Occurs when before writing an object. - /// - event Action ObjectWriteBefore; + event Action ObjectWriteBefore; - /// - /// Occurs when after writing an object. - /// - event Action ObjectWriteAfter; + /// + /// Occurs when after writing an object. + /// + event Action ObjectWriteAfter; - /// - /// Gets the object renderers that will render and elements. - /// - ObjectRendererCollection ObjectRenderers { get; } + /// + /// Gets the object renderers that will render and elements. + /// + ObjectRendererCollection ObjectRenderers { get; } - /// - /// Renders the specified markdown object. - /// - /// The markdown object. - /// The result of the rendering. - object Render(MarkdownObject markdownObject); - } + /// + /// Renders the specified markdown object. + /// + /// The markdown object. + /// The result of the rendering. + object Render(MarkdownObject markdownObject); } \ No newline at end of file diff --git a/src/Markdig/Renderers/MarkdownObjectRenderer.cs b/src/Markdig/Renderers/MarkdownObjectRenderer.cs index 638d05c74..941754742 100644 --- a/src/Markdig/Renderers/MarkdownObjectRenderer.cs +++ b/src/Markdig/Renderers/MarkdownObjectRenderer.cs @@ -4,65 +4,63 @@ using Markdig.Helpers; using Markdig.Syntax; -using System; -namespace Markdig.Renderers -{ - /// - /// A base class for rendering and Markdown objects. - /// - /// The type of the renderer. - /// The type of the object. - /// - public abstract class MarkdownObjectRenderer : IMarkdownObjectRenderer where TRenderer : RendererBase where TObject : MarkdownObject - { - private OrderedList? _tryWriters; +namespace Markdig.Renderers; - protected MarkdownObjectRenderer() { } +/// +/// A base class for rendering and Markdown objects. +/// +/// The type of the renderer. +/// The type of the object. +/// +public abstract class MarkdownObjectRenderer : IMarkdownObjectRenderer where TRenderer : RendererBase where TObject : MarkdownObject +{ + private OrderedList? _tryWriters; - public delegate bool TryWriteDelegate(TRenderer renderer, TObject obj); + protected MarkdownObjectRenderer() { } - public bool Accept(RendererBase renderer, Type objectType) - { - return typeof(TObject).IsAssignableFrom(objectType); - } + public delegate bool TryWriteDelegate(TRenderer renderer, TObject obj); - public virtual void Write(RendererBase renderer, MarkdownObject obj) - { - var htmlRenderer = (TRenderer)renderer; - var typedObj = (TObject)obj; + public bool Accept(RendererBase renderer, Type objectType) + { + return typeof(TObject).IsAssignableFrom(objectType); + } - if (_tryWriters is not null && TryWrite(htmlRenderer, typedObj)) - { - return; - } + public virtual void Write(RendererBase renderer, MarkdownObject obj) + { + var htmlRenderer = (TRenderer)renderer; + var typedObj = (TObject)obj; - Write(htmlRenderer, typedObj); + if (_tryWriters is not null && TryWrite(htmlRenderer, typedObj)) + { + return; } - private bool TryWrite(TRenderer renderer, TObject obj) + Write(htmlRenderer, typedObj); + } + + private bool TryWrite(TRenderer renderer, TObject obj) + { + for (int i = 0; i < _tryWriters!.Count; i++) { - for (int i = 0; i < _tryWriters!.Count; i++) + var tryWriter = _tryWriters[i]; + if (tryWriter(renderer, obj)) { - var tryWriter = _tryWriters[i]; - if (tryWriter(renderer, obj)) - { - return true; - } + return true; } - return false; } + return false; + } - /// - /// Gets the optional writers attached to this instance. - /// - public OrderedList TryWriters => _tryWriters ??= new(); + /// + /// Gets the optional writers attached to this instance. + /// + public OrderedList TryWriters => _tryWriters ??= new(); - /// - /// Writes the specified Markdown object to the renderer. - /// - /// The renderer. - /// The markdown object. - protected abstract void Write(TRenderer renderer, TObject obj); - } + /// + /// Writes the specified Markdown object to the renderer. + /// + /// The renderer. + /// The markdown object. + protected abstract void Write(TRenderer renderer, TObject obj); } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs index 423225296..624ece86c 100644 --- a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs @@ -2,54 +2,52 @@ // 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 Markdig.Syntax; -namespace Markdig.Renderers.Normalize +namespace Markdig.Renderers.Normalize; + +/// +/// An Normalize renderer for a and . +/// +/// +public class CodeBlockRenderer : NormalizeObjectRenderer { - /// - /// An Normalize renderer for a and . - /// - /// - public class CodeBlockRenderer : NormalizeObjectRenderer - { - public bool OutputAttributesOnPre { get; set; } + public bool OutputAttributesOnPre { get; set; } - protected override void Write(NormalizeRenderer renderer, CodeBlock obj) + protected override void Write(NormalizeRenderer renderer, CodeBlock obj) + { + if (obj is FencedCodeBlock fencedCodeBlock) { - if (obj is FencedCodeBlock fencedCodeBlock) + var fencedCharCount = Math.Min(fencedCodeBlock.OpeningFencedCharCount, fencedCodeBlock.ClosingFencedCharCount); + var opening = new string(fencedCodeBlock.FencedChar, fencedCharCount); + renderer.Write(opening); + if (fencedCodeBlock.Info != null) { - var fencedCharCount = Math.Min(fencedCodeBlock.OpeningFencedCharCount, fencedCodeBlock.ClosingFencedCharCount); - var opening = new string(fencedCodeBlock.FencedChar, fencedCharCount); - renderer.Write(opening); - if (fencedCodeBlock.Info != null) - { - renderer.Write(fencedCodeBlock.Info); - } - if (!string.IsNullOrEmpty(fencedCodeBlock.Arguments)) - { - renderer.Write(' ').Write(fencedCodeBlock.Arguments); - } - - /* TODO do we need this causes a empty space and would render html attributes to markdown. - var attributes = obj.TryGetAttributes(); - if (attributes != null) - { - renderer.Write(' '); - renderer.Write(attributes); - } - */ - renderer.WriteLine(); - - renderer.WriteLeafRawLines(obj, true); - renderer.Write(opening); + renderer.Write(fencedCodeBlock.Info); } - else + if (!string.IsNullOrEmpty(fencedCodeBlock.Arguments)) { - renderer.WriteLeafRawLines(obj, false, true); + renderer.Write(' ').Write(fencedCodeBlock.Arguments); } - renderer.FinishBlock(renderer.Options.EmptyLineAfterCodeBlock); + /* TODO do we need this causes a empty space and would render html attributes to markdown. + var attributes = obj.TryGetAttributes(); + if (attributes != null) + { + renderer.Write(' '); + renderer.Write(attributes); + } + */ + renderer.WriteLine(); + + renderer.WriteLeafRawLines(obj, true); + renderer.Write(opening); } + else + { + renderer.WriteLeafRawLines(obj, false, true); + } + + renderer.FinishBlock(renderer.Options.EmptyLineAfterCodeBlock); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/HeadingRenderer.cs b/src/Markdig/Renderers/Normalize/HeadingRenderer.cs index 100901781..8155eddd6 100644 --- a/src/Markdig/Renderers/Normalize/HeadingRenderer.cs +++ b/src/Markdig/Renderers/Normalize/HeadingRenderer.cs @@ -4,33 +4,32 @@ using Markdig.Syntax; -namespace Markdig.Renderers.Normalize +namespace Markdig.Renderers.Normalize; + +/// +/// An Normalize renderer for a . +/// +/// +public class HeadingRenderer : NormalizeObjectRenderer { - /// - /// An Normalize renderer for a . - /// - /// - public class HeadingRenderer : NormalizeObjectRenderer - { - private static readonly string[] HeadingTexts = { - "#", - "##", - "###", - "####", - "#####", - "######", - }; + private static readonly string[] HeadingTexts = { + "#", + "##", + "###", + "####", + "#####", + "######", + }; - protected override void Write(NormalizeRenderer renderer, HeadingBlock obj) - { - var headingText = obj.Level > 0 && obj.Level <= 6 - ? HeadingTexts[obj.Level - 1] - : new string('#', obj.Level); + protected override void Write(NormalizeRenderer renderer, HeadingBlock obj) + { + var headingText = obj.Level > 0 && obj.Level <= 6 + ? HeadingTexts[obj.Level - 1] + : new string('#', obj.Level); - renderer.Write(headingText).Write(' '); - renderer.WriteLeafInline(obj); + renderer.Write(headingText).Write(' '); + renderer.WriteLeafInline(obj); - renderer.FinishBlock(renderer.Options.EmptyLineAfterHeading); - } + renderer.FinishBlock(renderer.Options.EmptyLineAfterHeading); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs b/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs index 0cc59dc95..5d2f39718 100644 --- a/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs @@ -4,13 +4,12 @@ using Markdig.Syntax; -namespace Markdig.Renderers.Normalize +namespace Markdig.Renderers.Normalize; + +public class HtmlBlockRenderer : NormalizeObjectRenderer { - public class HtmlBlockRenderer : NormalizeObjectRenderer + protected override void Write(NormalizeRenderer renderer, HtmlBlock obj) { - protected override void Write(NormalizeRenderer renderer, HtmlBlock obj) - { - renderer.WriteLeafRawLines(obj, true, false); - } + renderer.WriteLeafRawLines(obj, true, false); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/Inlines/AutolinkInlineRenderer.cs b/src/Markdig/Renderers/Normalize/Inlines/AutolinkInlineRenderer.cs index 47c869df0..e7d4a59c0 100644 --- a/src/Markdig/Renderers/Normalize/Inlines/AutolinkInlineRenderer.cs +++ b/src/Markdig/Renderers/Normalize/Inlines/AutolinkInlineRenderer.cs @@ -4,17 +4,16 @@ using Markdig.Syntax.Inlines; -namespace Markdig.Renderers.Normalize.Inlines +namespace Markdig.Renderers.Normalize.Inlines; + +/// +/// A Normalize renderer for an . +/// +/// +public class AutolinkInlineRenderer : NormalizeObjectRenderer { - /// - /// A Normalize renderer for an . - /// - /// - public class AutolinkInlineRenderer : NormalizeObjectRenderer + protected override void Write(NormalizeRenderer renderer, AutolinkInline obj) { - protected override void Write(NormalizeRenderer renderer, AutolinkInline obj) - { - renderer.Write('<').Write(obj.Url).Write('>'); - } + renderer.Write('<').Write(obj.Url).Write('>'); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/Inlines/CodeInlineRenderer.cs b/src/Markdig/Renderers/Normalize/Inlines/CodeInlineRenderer.cs index 9560075f2..7fdf42536 100644 --- a/src/Markdig/Renderers/Normalize/Inlines/CodeInlineRenderer.cs +++ b/src/Markdig/Renderers/Normalize/Inlines/CodeInlineRenderer.cs @@ -4,52 +4,51 @@ using Markdig.Syntax.Inlines; -namespace Markdig.Renderers.Normalize.Inlines +namespace Markdig.Renderers.Normalize.Inlines; + +/// +/// A Normalize renderer for a . +/// +/// +public class CodeInlineRenderer : NormalizeObjectRenderer { - /// - /// A Normalize renderer for a . - /// - /// - public class CodeInlineRenderer : NormalizeObjectRenderer + protected override void Write(NormalizeRenderer renderer, CodeInline obj) { - protected override void Write(NormalizeRenderer renderer, CodeInline obj) + var delimiterCount = 0; + string content = obj.Content; + for (var i = 0; i < content.Length; i++) { - var delimiterCount = 0; - string content = obj.Content; - for (var i = 0; i < content.Length; i++) - { - var index = content.IndexOf(obj.Delimiter, i); - if (index == -1) break; - - var count = 1; - for (i = index + 1; i < content.Length; i++) - { - if (content[i] == obj.Delimiter) count++; - else break; - } + var index = content.IndexOf(obj.Delimiter, i); + if (index == -1) break; - if (delimiterCount < count) - delimiterCount = count; + var count = 1; + for (i = index + 1; i < content.Length; i++) + { + if (content[i] == obj.Delimiter) count++; + else break; } - var delimiterRun = new string(obj.Delimiter, delimiterCount + 1); - renderer.Write(delimiterRun); - if (content.Length != 0) + + if (delimiterCount < count) + delimiterCount = count; + } + var delimiterRun = new string(obj.Delimiter, delimiterCount + 1); + renderer.Write(delimiterRun); + if (content.Length != 0) + { + if (content[0] == obj.Delimiter) { - if (content[0] == obj.Delimiter) - { - renderer.Write(' '); - } - renderer.Write(content); - if (content[content.Length - 1] == obj.Delimiter) - { - renderer.Write(' '); - } + renderer.Write(' '); } - else + renderer.Write(content); + if (content[content.Length - 1] == obj.Delimiter) { renderer.Write(' '); } - renderer.Write(delimiterRun); } + else + { + renderer.Write(' '); + } + renderer.Write(delimiterRun); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/Inlines/DelimiterInlineRenderer.cs b/src/Markdig/Renderers/Normalize/Inlines/DelimiterInlineRenderer.cs index 5b30dd69d..0a55dcc71 100644 --- a/src/Markdig/Renderers/Normalize/Inlines/DelimiterInlineRenderer.cs +++ b/src/Markdig/Renderers/Normalize/Inlines/DelimiterInlineRenderer.cs @@ -4,18 +4,17 @@ using Markdig.Syntax.Inlines; -namespace Markdig.Renderers.Normalize.Inlines +namespace Markdig.Renderers.Normalize.Inlines; + +/// +/// A Normalize renderer for a . +/// +/// +public class DelimiterInlineRenderer : NormalizeObjectRenderer { - /// - /// A Normalize renderer for a . - /// - /// - public class DelimiterInlineRenderer : NormalizeObjectRenderer + protected override void Write(NormalizeRenderer renderer, DelimiterInline obj) { - protected override void Write(NormalizeRenderer renderer, DelimiterInline obj) - { - renderer.Write(obj.ToLiteral()); - renderer.WriteChildren(obj); - } + renderer.Write(obj.ToLiteral()); + renderer.WriteChildren(obj); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/Inlines/EmphasisInlineRenderer.cs b/src/Markdig/Renderers/Normalize/Inlines/EmphasisInlineRenderer.cs index 7f08eb054..3e6a4328a 100644 --- a/src/Markdig/Renderers/Normalize/Inlines/EmphasisInlineRenderer.cs +++ b/src/Markdig/Renderers/Normalize/Inlines/EmphasisInlineRenderer.cs @@ -4,20 +4,19 @@ using Markdig.Syntax.Inlines; -namespace Markdig.Renderers.Normalize.Inlines +namespace Markdig.Renderers.Normalize.Inlines; + +/// +/// A Normalize renderer for an . +/// +/// +public class EmphasisInlineRenderer : NormalizeObjectRenderer { - /// - /// A Normalize renderer for an . - /// - /// - public class EmphasisInlineRenderer : NormalizeObjectRenderer + protected override void Write(NormalizeRenderer renderer, EmphasisInline obj) { - protected override void Write(NormalizeRenderer renderer, EmphasisInline obj) - { - var emphasisText = new string(obj.DelimiterChar, obj.DelimiterCount); - renderer.Write(emphasisText); - renderer.WriteChildren(obj); - renderer.Write(emphasisText); - } + var emphasisText = new string(obj.DelimiterChar, obj.DelimiterCount); + renderer.Write(emphasisText); + renderer.WriteChildren(obj); + renderer.Write(emphasisText); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/Inlines/LineBreakInlineRenderer.cs b/src/Markdig/Renderers/Normalize/Inlines/LineBreakInlineRenderer.cs index ba3f94ed9..2519ce2ec 100644 --- a/src/Markdig/Renderers/Normalize/Inlines/LineBreakInlineRenderer.cs +++ b/src/Markdig/Renderers/Normalize/Inlines/LineBreakInlineRenderer.cs @@ -4,26 +4,25 @@ using Markdig.Syntax.Inlines; -namespace Markdig.Renderers.Normalize.Inlines +namespace Markdig.Renderers.Normalize.Inlines; + +/// +/// A Normalize renderer for a . +/// +/// +public class LineBreakInlineRenderer : NormalizeObjectRenderer { /// - /// A Normalize renderer for a . + /// Gets or sets a value indicating whether to render this softline break as a Normalize hardline break tag (<br />) /// - /// - public class LineBreakInlineRenderer : NormalizeObjectRenderer - { - /// - /// Gets or sets a value indicating whether to render this softline break as a Normalize hardline break tag (<br />) - /// - public bool RenderAsHardlineBreak { get; set; } + public bool RenderAsHardlineBreak { get; set; } - protected override void Write(NormalizeRenderer renderer, LineBreakInline obj) + protected override void Write(NormalizeRenderer renderer, LineBreakInline obj) + { + if (obj.IsHard) { - if (obj.IsHard) - { - renderer.Write(obj.IsBackslash ? "\\" : " "); - } - renderer.WriteLine(); + renderer.Write(obj.IsBackslash ? "\\" : " "); } + renderer.WriteLine(); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs b/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs index adfdb1879..d256b7178 100644 --- a/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs +++ b/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs @@ -4,61 +4,60 @@ using Markdig.Syntax.Inlines; -namespace Markdig.Renderers.Normalize.Inlines +namespace Markdig.Renderers.Normalize.Inlines; + +/// +/// A Normalize renderer for a . +/// +/// +public class LinkInlineRenderer : NormalizeObjectRenderer { - /// - /// A Normalize renderer for a . - /// - /// - public class LinkInlineRenderer : NormalizeObjectRenderer + protected override void Write(NormalizeRenderer renderer, LinkInline link) { - protected override void Write(NormalizeRenderer renderer, LinkInline link) + if (link.IsAutoLink && !renderer.Options.ExpandAutoLinks) { - if (link.IsAutoLink && !renderer.Options.ExpandAutoLinks) - { - renderer.Write(link.Url); - return; - } + renderer.Write(link.Url); + return; + } - if (link.IsImage) - { - renderer.Write('!'); - } - renderer.Write('['); - renderer.WriteChildren(link); - renderer.Write(']'); + if (link.IsImage) + { + renderer.Write('!'); + } + renderer.Write('['); + renderer.WriteChildren(link); + renderer.Write(']'); - if (link.Label != null) + if (link.Label != null) + { + if (link.FirstChild is LiteralInline literal && literal.Content.Length == link.Label.Length && literal.Content.Match(link.Label)) { - if (link.FirstChild is LiteralInline literal && literal.Content.Length == link.Label.Length && literal.Content.Match(link.Label)) + // collapsed reference and shortcut links + if (!link.IsShortcut) { - // collapsed reference and shortcut links - if (!link.IsShortcut) - { - renderer.Write("[]"); - } - } - else - { - // full link - renderer.Write('[').Write(link.Label).Write(']'); + renderer.Write("[]"); } } else { - if (!string.IsNullOrEmpty(link.Url)) - { - renderer.Write('(').Write(link.Url); - - if (link.Title is { Length: > 0 }) - { - renderer.Write(" \""); - renderer.Write(link.Title.Replace(@"""", @"\""")); - renderer.Write('"'); - } + // full link + renderer.Write('[').Write(link.Label).Write(']'); + } + } + else + { + if (!string.IsNullOrEmpty(link.Url)) + { + renderer.Write('(').Write(link.Url); - renderer.Write(')'); + if (link.Title is { Length: > 0 }) + { + renderer.Write(" \""); + renderer.Write(link.Title.Replace(@"""", @"\""")); + renderer.Write('"'); } + + renderer.Write(')'); } } } diff --git a/src/Markdig/Renderers/Normalize/Inlines/LiteralInlineRenderer.cs b/src/Markdig/Renderers/Normalize/Inlines/LiteralInlineRenderer.cs index 90cd9f263..db7cf5f24 100644 --- a/src/Markdig/Renderers/Normalize/Inlines/LiteralInlineRenderer.cs +++ b/src/Markdig/Renderers/Normalize/Inlines/LiteralInlineRenderer.cs @@ -5,21 +5,20 @@ using Markdig.Helpers; using Markdig.Syntax.Inlines; -namespace Markdig.Renderers.Normalize.Inlines +namespace Markdig.Renderers.Normalize.Inlines; + +/// +/// A Normalize renderer for a . +/// +/// +public class LiteralInlineRenderer : NormalizeObjectRenderer { - /// - /// A Normalize renderer for a . - /// - /// - public class LiteralInlineRenderer : NormalizeObjectRenderer + protected override void Write(NormalizeRenderer renderer, LiteralInline obj) { - protected override void Write(NormalizeRenderer renderer, LiteralInline obj) + if (obj.IsFirstCharacterEscaped && obj.Content.Length > 0 && obj.Content[obj.Content.Start].IsAsciiPunctuation()) { - if (obj.IsFirstCharacterEscaped && obj.Content.Length > 0 && obj.Content[obj.Content.Start].IsAsciiPunctuation()) - { - renderer.Write('\\'); - } - renderer.Write(ref obj.Content); + renderer.Write('\\'); } + renderer.Write(ref obj.Content); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/Inlines/NormalizeHtmlEntityInlineRenderer.cs b/src/Markdig/Renderers/Normalize/Inlines/NormalizeHtmlEntityInlineRenderer.cs index 8285de36c..ca6bff9c0 100644 --- a/src/Markdig/Renderers/Normalize/Inlines/NormalizeHtmlEntityInlineRenderer.cs +++ b/src/Markdig/Renderers/Normalize/Inlines/NormalizeHtmlEntityInlineRenderer.cs @@ -4,16 +4,15 @@ using Markdig.Syntax.Inlines; -namespace Markdig.Renderers.Normalize.Inlines +namespace Markdig.Renderers.Normalize.Inlines; + +/// +/// A Normalize renderer for a . +/// +public class NormalizeHtmlEntityInlineRenderer : NormalizeObjectRenderer { - /// - /// A Normalize renderer for a . - /// - public class NormalizeHtmlEntityInlineRenderer : NormalizeObjectRenderer + protected override void Write(NormalizeRenderer renderer, HtmlEntityInline obj) { - protected override void Write(NormalizeRenderer renderer, HtmlEntityInline obj) - { - renderer.Write(obj.Original); - } + renderer.Write(obj.Original); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/Inlines/NormalizeHtmlInlineRenderer.cs b/src/Markdig/Renderers/Normalize/Inlines/NormalizeHtmlInlineRenderer.cs index 651327c87..d167a4efb 100644 --- a/src/Markdig/Renderers/Normalize/Inlines/NormalizeHtmlInlineRenderer.cs +++ b/src/Markdig/Renderers/Normalize/Inlines/NormalizeHtmlInlineRenderer.cs @@ -4,16 +4,15 @@ using Markdig.Syntax.Inlines; -namespace Markdig.Renderers.Normalize.Inlines +namespace Markdig.Renderers.Normalize.Inlines; + +/// +/// A Normalize renderer for a . +/// +public class NormalizeHtmlInlineRenderer : NormalizeObjectRenderer { - /// - /// A Normalize renderer for a . - /// - public class NormalizeHtmlInlineRenderer : NormalizeObjectRenderer + protected override void Write(NormalizeRenderer renderer, HtmlInline obj) { - protected override void Write(NormalizeRenderer renderer, HtmlInline obj) - { - renderer.Write(obj.Tag); - } + renderer.Write(obj.Tag); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionGroupRenderer.cs b/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionGroupRenderer.cs index 8d1a7ca2d..da385bc97 100644 --- a/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionGroupRenderer.cs +++ b/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionGroupRenderer.cs @@ -4,15 +4,14 @@ using Markdig.Syntax; -namespace Markdig.Renderers.Normalize +namespace Markdig.Renderers.Normalize; + +public class LinkReferenceDefinitionGroupRenderer : NormalizeObjectRenderer { - public class LinkReferenceDefinitionGroupRenderer : NormalizeObjectRenderer + protected override void Write(NormalizeRenderer renderer, LinkReferenceDefinitionGroup obj) { - protected override void Write(NormalizeRenderer renderer, LinkReferenceDefinitionGroup obj) - { - renderer.EnsureLine(); - renderer.WriteChildren(obj); - renderer.FinishBlock(false); - } + renderer.EnsureLine(); + renderer.WriteChildren(obj); + renderer.FinishBlock(false); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionRenderer.cs b/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionRenderer.cs index c09a9bbde..f21dba9b3 100644 --- a/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionRenderer.cs +++ b/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionRenderer.cs @@ -4,26 +4,25 @@ using Markdig.Syntax; -namespace Markdig.Renderers.Normalize +namespace Markdig.Renderers.Normalize; + +public class LinkReferenceDefinitionRenderer : NormalizeObjectRenderer { - public class LinkReferenceDefinitionRenderer : NormalizeObjectRenderer + protected override void Write(NormalizeRenderer renderer, LinkReferenceDefinition linkDef) { - protected override void Write(NormalizeRenderer renderer, LinkReferenceDefinition linkDef) - { - renderer.EnsureLine(); - renderer.Write('['); - renderer.Write(linkDef.Label); - renderer.Write("]: "); + renderer.EnsureLine(); + renderer.Write('['); + renderer.Write(linkDef.Label); + renderer.Write("]: "); - renderer.Write(linkDef.Url); + renderer.Write(linkDef.Url); - if (linkDef.Title != null) - { - renderer.Write(" \""); - renderer.Write(linkDef.Title.Replace("\"", "\\\"")); - renderer.Write('"'); - } - renderer.FinishBlock(false); + if (linkDef.Title != null) + { + renderer.Write(" \""); + renderer.Write(linkDef.Title.Replace("\"", "\\\"")); + renderer.Write('"'); } + renderer.FinishBlock(false); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/ListRenderer.cs b/src/Markdig/Renderers/Normalize/ListRenderer.cs index 71b2c4749..e4dea8b38 100644 --- a/src/Markdig/Renderers/Normalize/ListRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ListRenderer.cs @@ -5,90 +5,89 @@ using System.Globalization; using Markdig.Syntax; -namespace Markdig.Renderers.Normalize +namespace Markdig.Renderers.Normalize; + +/// +/// A Normalize renderer for a . +/// +/// +public class ListRenderer : NormalizeObjectRenderer { - /// - /// A Normalize renderer for a . - /// - /// - public class ListRenderer : NormalizeObjectRenderer + protected override void Write(NormalizeRenderer renderer, ListBlock listBlock) { - protected override void Write(NormalizeRenderer renderer, ListBlock listBlock) + renderer.EnsureLine(); + var compact = renderer.CompactParagraph; + renderer.CompactParagraph = !listBlock.IsLoose; + if (listBlock.IsOrdered) { - renderer.EnsureLine(); - var compact = renderer.CompactParagraph; - renderer.CompactParagraph = !listBlock.IsLoose; - if (listBlock.IsOrdered) + int index = 0; + if (listBlock.OrderedStart != null) { - int index = 0; - if (listBlock.OrderedStart != null) + switch (listBlock.BulletType) { - switch (listBlock.BulletType) - { - case '1': - int.TryParse(listBlock.OrderedStart, out index); - break; - } + case '1': + int.TryParse(listBlock.OrderedStart, out index); + break; } - for (var i = 0; i < listBlock.Count; i++) + } + for (var i = 0; i < listBlock.Count; i++) + { + var item = listBlock[i]; + var listItem = (ListItemBlock) item; + renderer.EnsureLine(); + + renderer.Write(index.ToString(CultureInfo.InvariantCulture)); + renderer.Write(listBlock.OrderedDelimiter); + renderer.Write(' '); + renderer.PushIndent(new string(' ', IntLog10Fast(index) + 3)); + renderer.WriteChildren(listItem); + renderer.PopIndent(); + switch (listBlock.BulletType) + { + case '1': + index++; + break; + } + if (i + 1 < listBlock.Count && listBlock.IsLoose) { - var item = listBlock[i]; - var listItem = (ListItemBlock) item; renderer.EnsureLine(); - - renderer.Write(index.ToString(CultureInfo.InvariantCulture)); - renderer.Write(listBlock.OrderedDelimiter); - renderer.Write(' '); - renderer.PushIndent(new string(' ', IntLog10Fast(index) + 3)); - renderer.WriteChildren(listItem); - renderer.PopIndent(); - switch (listBlock.BulletType) - { - case '1': - index++; - break; - } - if (i + 1 < listBlock.Count && listBlock.IsLoose) - { - renderer.EnsureLine(); - renderer.WriteLine(); - } + renderer.WriteLine(); } } - else + } + else + { + for (var i = 0; i < listBlock.Count; i++) { - for (var i = 0; i < listBlock.Count; i++) + var item = listBlock[i]; + var listItem = (ListItemBlock) item; + renderer.EnsureLine(); + renderer.Write(renderer.Options.ListItemCharacter ?? listBlock.BulletType); + renderer.Write(' '); + renderer.PushIndent(" "); + renderer.WriteChildren(listItem); + renderer.PopIndent(); + if (i + 1 < listBlock.Count && listBlock.IsLoose) { - var item = listBlock[i]; - var listItem = (ListItemBlock) item; renderer.EnsureLine(); - renderer.Write(renderer.Options.ListItemCharacter ?? listBlock.BulletType); - renderer.Write(' '); - renderer.PushIndent(" "); - renderer.WriteChildren(listItem); - renderer.PopIndent(); - if (i + 1 < listBlock.Count && listBlock.IsLoose) - { - renderer.EnsureLine(); - renderer.WriteLine(); - } + renderer.WriteLine(); } } - renderer.CompactParagraph = compact; - - renderer.FinishBlock(true); } + renderer.CompactParagraph = compact; - - private static int IntLog10Fast(int input) => - (input < 10) ? 0 : - (input < 100) ? 1 : - (input < 1000) ? 2 : - (input < 10000) ? 3 : - (input < 100000) ? 4 : - (input < 1000000) ? 5 : - (input < 10000000) ? 6 : - (input < 100000000) ? 7 : - (input < 1000000000) ? 8 : 9; + renderer.FinishBlock(true); } + + + private static int IntLog10Fast(int input) => + (input < 10) ? 0 : + (input < 100) ? 1 : + (input < 1000) ? 2 : + (input < 10000) ? 3 : + (input < 100000) ? 4 : + (input < 1000000) ? 5 : + (input < 10000000) ? 6 : + (input < 100000000) ? 7 : + (input < 1000000000) ? 8 : 9; } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/NormalizeObjectRenderer.cs b/src/Markdig/Renderers/Normalize/NormalizeObjectRenderer.cs index 9eac1e94a..61ea8352c 100644 --- a/src/Markdig/Renderers/Normalize/NormalizeObjectRenderer.cs +++ b/src/Markdig/Renderers/Normalize/NormalizeObjectRenderer.cs @@ -4,14 +4,13 @@ using Markdig.Syntax; -namespace Markdig.Renderers.Normalize +namespace Markdig.Renderers.Normalize; + +/// +/// A base class for Normalize rendering and Markdown objects. +/// +/// The type of the object. +/// +public abstract class NormalizeObjectRenderer : MarkdownObjectRenderer where TObject : MarkdownObject { - /// - /// A base class for Normalize rendering and Markdown objects. - /// - /// The type of the object. - /// - public abstract class NormalizeObjectRenderer : MarkdownObjectRenderer where TObject : MarkdownObject - { - } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/NormalizeOptions.cs b/src/Markdig/Renderers/Normalize/NormalizeOptions.cs index b836cfea8..6bcb81108 100644 --- a/src/Markdig/Renderers/Normalize/NormalizeOptions.cs +++ b/src/Markdig/Renderers/Normalize/NormalizeOptions.cs @@ -2,54 +2,53 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -namespace Markdig.Renderers.Normalize +namespace Markdig.Renderers.Normalize; + +/// +/// Defines the options used by +/// +public class NormalizeOptions { /// - /// Defines the options used by + /// Initialize a new instance of /// - public class NormalizeOptions + public NormalizeOptions() { - /// - /// Initialize a new instance of - /// - public NormalizeOptions() - { - SpaceAfterQuoteBlock = true; - EmptyLineAfterCodeBlock = true; - EmptyLineAfterHeading = true; - EmptyLineAfterThematicBreak = true; - ExpandAutoLinks = true; - ListItemCharacter = null; - } - - /// - /// Adds a space after a QuoteBlock >. Default is true - /// - public bool SpaceAfterQuoteBlock { get; set; } - - /// - /// Adds an empty line after a code block (fenced and tabbed). Default is true - /// - public bool EmptyLineAfterCodeBlock { get; set; } - - /// - /// Adds an empty line after an heading. Default is true - /// - public bool EmptyLineAfterHeading { get; set; } - - /// - /// Adds an empty line after an thematic break. Default is true - /// - public bool EmptyLineAfterThematicBreak { get; set; } - - /// - /// The bullet character used for list items. Default is null leaving the original bullet character as-is. - /// - public char? ListItemCharacter { get; set; } - - /// - /// Expands AutoLinks to the normal inline representation. Default is true - /// - public bool ExpandAutoLinks { get; set; } + SpaceAfterQuoteBlock = true; + EmptyLineAfterCodeBlock = true; + EmptyLineAfterHeading = true; + EmptyLineAfterThematicBreak = true; + ExpandAutoLinks = true; + ListItemCharacter = null; } + + /// + /// Adds a space after a QuoteBlock >. Default is true + /// + public bool SpaceAfterQuoteBlock { get; set; } + + /// + /// Adds an empty line after a code block (fenced and tabbed). Default is true + /// + public bool EmptyLineAfterCodeBlock { get; set; } + + /// + /// Adds an empty line after an heading. Default is true + /// + public bool EmptyLineAfterHeading { get; set; } + + /// + /// Adds an empty line after an thematic break. Default is true + /// + public bool EmptyLineAfterThematicBreak { get; set; } + + /// + /// The bullet character used for list items. Default is null leaving the original bullet character as-is. + /// + public char? ListItemCharacter { get; set; } + + /// + /// Expands AutoLinks to the normal inline representation. Default is true + /// + public bool ExpandAutoLinks { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs b/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs index 00360acb5..abb426020 100644 --- a/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs +++ b/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs @@ -7,156 +7,155 @@ using Markdig.Renderers.Normalize.Inlines; using Markdig.Helpers; -namespace Markdig.Renderers.Normalize +namespace Markdig.Renderers.Normalize; + +/// +/// Default HTML renderer for a Markdown object. +/// +/// +public class NormalizeRenderer : TextRendererBase { /// - /// Default HTML renderer for a Markdown object. + /// Initializes a new instance of the class. /// - /// - public class NormalizeRenderer : TextRendererBase + /// The writer. + /// The normalize options + public NormalizeRenderer(TextWriter writer, NormalizeOptions? options = null) : base(writer) + { + Options = options ?? new NormalizeOptions(); + // Default block renderers + ObjectRenderers.Add(new CodeBlockRenderer()); + ObjectRenderers.Add(new ListRenderer()); + ObjectRenderers.Add(new HeadingRenderer()); + ObjectRenderers.Add(new HtmlBlockRenderer()); + ObjectRenderers.Add(new ParagraphRenderer()); + ObjectRenderers.Add(new QuoteBlockRenderer()); + ObjectRenderers.Add(new ThematicBreakRenderer()); + ObjectRenderers.Add(new LinkReferenceDefinitionGroupRenderer()); + ObjectRenderers.Add(new LinkReferenceDefinitionRenderer()); + + // Default inline renderers + ObjectRenderers.Add(new AutolinkInlineRenderer()); + ObjectRenderers.Add(new CodeInlineRenderer()); + ObjectRenderers.Add(new DelimiterInlineRenderer()); + ObjectRenderers.Add(new EmphasisInlineRenderer()); + ObjectRenderers.Add(new LineBreakInlineRenderer()); + ObjectRenderers.Add(new NormalizeHtmlInlineRenderer()); + ObjectRenderers.Add(new NormalizeHtmlEntityInlineRenderer()); + ObjectRenderers.Add(new LinkInlineRenderer()); + ObjectRenderers.Add(new LiteralInlineRenderer()); + } + + public NormalizeOptions Options { get; } + + public bool CompactParagraph { get; set; } + + public void FinishBlock(bool emptyLine) { - /// - /// Initializes a new instance of the class. - /// - /// The writer. - /// The normalize options - public NormalizeRenderer(TextWriter writer, NormalizeOptions? options = null) : base(writer) + if (!IsLastInContainer) { - Options = options ?? new NormalizeOptions(); - // Default block renderers - ObjectRenderers.Add(new CodeBlockRenderer()); - ObjectRenderers.Add(new ListRenderer()); - ObjectRenderers.Add(new HeadingRenderer()); - ObjectRenderers.Add(new HtmlBlockRenderer()); - ObjectRenderers.Add(new ParagraphRenderer()); - ObjectRenderers.Add(new QuoteBlockRenderer()); - ObjectRenderers.Add(new ThematicBreakRenderer()); - ObjectRenderers.Add(new LinkReferenceDefinitionGroupRenderer()); - ObjectRenderers.Add(new LinkReferenceDefinitionRenderer()); - - // Default inline renderers - ObjectRenderers.Add(new AutolinkInlineRenderer()); - ObjectRenderers.Add(new CodeInlineRenderer()); - ObjectRenderers.Add(new DelimiterInlineRenderer()); - ObjectRenderers.Add(new EmphasisInlineRenderer()); - ObjectRenderers.Add(new LineBreakInlineRenderer()); - ObjectRenderers.Add(new NormalizeHtmlInlineRenderer()); - ObjectRenderers.Add(new NormalizeHtmlEntityInlineRenderer()); - ObjectRenderers.Add(new LinkInlineRenderer()); - ObjectRenderers.Add(new LiteralInlineRenderer()); + WriteLine(); + if (emptyLine) + { + WriteLine(); + } } + } + + ///// + ///// Writes the attached on the specified . + ///// + ///// The object. + ///// + //public NormalizeRenderer WriteAttributes(MarkdownObject obj) + //{ + // if (obj is null) throw new ArgumentNullException(nameof(obj)); + // return WriteAttributes(obj.TryGetAttributes()); + //} + + ///// + ///// Writes the specified . + ///// + ///// The attributes to render. + ///// This instance + //public NormalizeRenderer WriteAttributes(HtmlAttributes attributes) + //{ + // if (attributes is null) + // { + // return this; + // } + + // if (attributes.Id != null) + // { + // Write(" id=\"").WriteEscape(attributes.Id).Write('"'); + // } + + // if (attributes.Classes != null && attributes.Classes.Count > 0) + // { + // Write(" class=\""); + // for (int i = 0; i < attributes.Classes.Count; i++) + // { + // var cssClass = attributes.Classes[i]; + // if (i > 0) + // { + // Write(" "); + // } + // WriteEscape(cssClass); + // } + // Write('"'); + // } + + // if (attributes.Properties != null && attributes.Properties.Count > 0) + // { + // foreach (var property in attributes.Properties) + // { + // Write(' ').Write(property.Key); + // if (property.Value != null) + // { + // Write('=').Write('"'); + // WriteEscape(property.Value); + // Write('"'); + // } + // } + // } + + // return this; + //} - public NormalizeOptions Options { get; } - - public bool CompactParagraph { get; set; } - - public void FinishBlock(bool emptyLine) + /// + /// Writes the lines of a + /// + /// The leaf block. + /// if set to true write end of lines. + /// Whether to write indents. + /// This instance + public NormalizeRenderer WriteLeafRawLines(LeafBlock leafBlock, bool writeEndOfLines, bool indent = false) + { + if (leafBlock is null) ThrowHelper.ArgumentNullException_leafBlock(); + if (leafBlock.Lines.Lines != null) { - if (!IsLastInContainer) + var lines = leafBlock.Lines; + var slices = lines.Lines; + for (int i = 0; i < lines.Count; i++) { - WriteLine(); - if (emptyLine) + if (!writeEndOfLines && i > 0) { WriteLine(); } - } - } - ///// - ///// Writes the attached on the specified . - ///// - ///// The object. - ///// - //public NormalizeRenderer WriteAttributes(MarkdownObject obj) - //{ - // if (obj is null) throw new ArgumentNullException(nameof(obj)); - // return WriteAttributes(obj.TryGetAttributes()); - //} - - ///// - ///// Writes the specified . - ///// - ///// The attributes to render. - ///// This instance - //public NormalizeRenderer WriteAttributes(HtmlAttributes attributes) - //{ - // if (attributes is null) - // { - // return this; - // } - - // if (attributes.Id != null) - // { - // Write(" id=\"").WriteEscape(attributes.Id).Write('"'); - // } - - // if (attributes.Classes != null && attributes.Classes.Count > 0) - // { - // Write(" class=\""); - // for (int i = 0; i < attributes.Classes.Count; i++) - // { - // var cssClass = attributes.Classes[i]; - // if (i > 0) - // { - // Write(" "); - // } - // WriteEscape(cssClass); - // } - // Write('"'); - // } - - // if (attributes.Properties != null && attributes.Properties.Count > 0) - // { - // foreach (var property in attributes.Properties) - // { - // Write(' ').Write(property.Key); - // if (property.Value != null) - // { - // Write('=').Write('"'); - // WriteEscape(property.Value); - // Write('"'); - // } - // } - // } - - // return this; - //} - - /// - /// Writes the lines of a - /// - /// The leaf block. - /// if set to true write end of lines. - /// Whether to write indents. - /// This instance - public NormalizeRenderer WriteLeafRawLines(LeafBlock leafBlock, bool writeEndOfLines, bool indent = false) - { - if (leafBlock is null) ThrowHelper.ArgumentNullException_leafBlock(); - if (leafBlock.Lines.Lines != null) - { - var lines = leafBlock.Lines; - var slices = lines.Lines; - for (int i = 0; i < lines.Count; i++) + if (indent) { - if (!writeEndOfLines && i > 0) - { - WriteLine(); - } - - if (indent) - { - Write(" "); - } - - Write(ref slices[i].Slice); - - if (writeEndOfLines) - { - WriteLine(); - } + Write(" "); + } + + Write(ref slices[i].Slice); + + if (writeEndOfLines) + { + WriteLine(); } } - return this; } - } + return this; + } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs b/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs index 6fa0f65af..62f21add7 100644 --- a/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs @@ -4,18 +4,17 @@ using Markdig.Syntax; -namespace Markdig.Renderers.Normalize +namespace Markdig.Renderers.Normalize; + +/// +/// A Normalize renderer for a . +/// +/// +public class ParagraphRenderer : NormalizeObjectRenderer { - /// - /// A Normalize renderer for a . - /// - /// - public class ParagraphRenderer : NormalizeObjectRenderer + protected override void Write(NormalizeRenderer renderer, ParagraphBlock obj) { - protected override void Write(NormalizeRenderer renderer, ParagraphBlock obj) - { - renderer.WriteLeafInline(obj); - renderer.FinishBlock(!renderer.CompactParagraph); - } + renderer.WriteLeafInline(obj); + renderer.FinishBlock(!renderer.CompactParagraph); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs b/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs index 345b99341..61b2dcb24 100644 --- a/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs @@ -4,22 +4,21 @@ using Markdig.Syntax; -namespace Markdig.Renderers.Normalize +namespace Markdig.Renderers.Normalize; + +/// +/// A Normalize renderer for a . +/// +/// +public class QuoteBlockRenderer : NormalizeObjectRenderer { - /// - /// A Normalize renderer for a . - /// - /// - public class QuoteBlockRenderer : NormalizeObjectRenderer + protected override void Write(NormalizeRenderer renderer, QuoteBlock obj) { - protected override void Write(NormalizeRenderer renderer, QuoteBlock obj) - { - var quoteIndent = renderer.Options.SpaceAfterQuoteBlock ? obj.QuoteChar + " " : obj.QuoteChar.ToString(); - renderer.PushIndent(quoteIndent); - renderer.WriteChildren(obj); - renderer.PopIndent(); + var quoteIndent = renderer.Options.SpaceAfterQuoteBlock ? obj.QuoteChar + " " : obj.QuoteChar.ToString(); + renderer.PushIndent(quoteIndent); + renderer.WriteChildren(obj); + renderer.PopIndent(); - renderer.FinishBlock(true); - } + renderer.FinishBlock(true); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/ThematicBreakRenderer.cs b/src/Markdig/Renderers/Normalize/ThematicBreakRenderer.cs index 1202b098d..9b1392465 100644 --- a/src/Markdig/Renderers/Normalize/ThematicBreakRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ThematicBreakRenderer.cs @@ -4,19 +4,18 @@ using Markdig.Syntax; -namespace Markdig.Renderers.Normalize +namespace Markdig.Renderers.Normalize; + +/// +/// A Normalize renderer for a . +/// +/// +public class ThematicBreakRenderer : NormalizeObjectRenderer { - /// - /// A Normalize renderer for a . - /// - /// - public class ThematicBreakRenderer : NormalizeObjectRenderer + protected override void Write(NormalizeRenderer renderer, ThematicBreakBlock obj) { - protected override void Write(NormalizeRenderer renderer, ThematicBreakBlock obj) - { - renderer.WriteLine(new string(obj.ThematicChar, obj.ThematicCharCount)); + renderer.WriteLine(new string(obj.ThematicChar, obj.ThematicCharCount)); - renderer.FinishBlock(renderer.Options.EmptyLineAfterThematicBreak); - } + renderer.FinishBlock(renderer.Options.EmptyLineAfterThematicBreak); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/ObjectRendererCollection.cs b/src/Markdig/Renderers/ObjectRendererCollection.cs index e99e465b7..8dcdeb6c4 100644 --- a/src/Markdig/Renderers/ObjectRendererCollection.cs +++ b/src/Markdig/Renderers/ObjectRendererCollection.cs @@ -4,13 +4,12 @@ using Markdig.Helpers; -namespace Markdig.Renderers +namespace Markdig.Renderers; + +/// +/// A collection of . +/// +/// +public class ObjectRendererCollection : OrderedList { - /// - /// A collection of . - /// - /// - public class ObjectRendererCollection : OrderedList - { - } } \ No newline at end of file diff --git a/src/Markdig/Renderers/RendererBase.cs b/src/Markdig/Renderers/RendererBase.cs index f5f26c7b0..fe9ce8555 100644 --- a/src/Markdig/Renderers/RendererBase.cs +++ b/src/Markdig/Renderers/RendererBase.cs @@ -2,185 +2,183 @@ // 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.Collections.Generic; using System.Runtime.CompilerServices; + using Markdig.Helpers; using Markdig.Syntax; using Markdig.Syntax.Inlines; -namespace Markdig.Renderers +namespace Markdig.Renderers; + +/// +/// Base class for a . +/// +/// +public abstract class RendererBase : IMarkdownRenderer { + private readonly Dictionary _renderersPerType = new(); + internal int _childrenDepth = 0; + /// - /// Base class for a . + /// Initializes a new instance of the class. /// - /// - public abstract class RendererBase : IMarkdownRenderer - { - private readonly Dictionary _renderersPerType = new(); - internal int _childrenDepth = 0; + protected RendererBase() { } - /// - /// Initializes a new instance of the class. - /// - protected RendererBase() { } + private IMarkdownObjectRenderer? GetRendererInstance(MarkdownObject obj) + { + KeyWrapper key = GetKeyForType(obj); + Type objectType = obj.GetType(); - private IMarkdownObjectRenderer? GetRendererInstance(MarkdownObject obj) + for (int i = 0; i < ObjectRenderers.Count; i++) { - KeyWrapper key = GetKeyForType(obj); - Type objectType = obj.GetType(); - - for (int i = 0; i < ObjectRenderers.Count; i++) + var renderer = ObjectRenderers[i]; + if (renderer.Accept(this, objectType)) { - var renderer = ObjectRenderers[i]; - if (renderer.Accept(this, objectType)) - { - _renderersPerType[key] = renderer; - return renderer; - } + _renderersPerType[key] = renderer; + return renderer; } - - _renderersPerType[key] = null; - return null; } - public ObjectRendererCollection ObjectRenderers { get; } = new(); - - public abstract object Render(MarkdownObject markdownObject); + _renderersPerType[key] = null; + return null; + } - public bool IsFirstInContainer { get; private set; } + public ObjectRendererCollection ObjectRenderers { get; } = new(); - public bool IsLastInContainer { get; private set; } + public abstract object Render(MarkdownObject markdownObject); - /// - /// Occurs when before writing an object. - /// - public event Action? ObjectWriteBefore; + public bool IsFirstInContainer { get; private set; } - /// - /// Occurs when after writing an object. - /// - public event Action? ObjectWriteAfter; + public bool IsLastInContainer { get; private set; } - /// - /// Writes the children of the specified . - /// - /// The container block. - public void WriteChildren(ContainerBlock containerBlock) - { - if (containerBlock is null) - { - return; - } + /// + /// Occurs when before writing an object. + /// + public event Action? ObjectWriteBefore; - ThrowHelper.CheckDepthLimit(_childrenDepth++); + /// + /// Occurs when after writing an object. + /// + public event Action? ObjectWriteAfter; - bool saveIsFirstInContainer = IsFirstInContainer; - bool saveIsLastInContainer = IsLastInContainer; + /// + /// Writes the children of the specified . + /// + /// The container block. + public void WriteChildren(ContainerBlock containerBlock) + { + if (containerBlock is null) + { + return; + } - var children = containerBlock; - for (int i = 0; i < children.Count; i++) - { - IsFirstInContainer = i == 0; - IsLastInContainer = i + 1 == children.Count; - Write(children[i]); - } + ThrowHelper.CheckDepthLimit(_childrenDepth++); - IsFirstInContainer = saveIsFirstInContainer; - IsLastInContainer = saveIsLastInContainer; + bool saveIsFirstInContainer = IsFirstInContainer; + bool saveIsLastInContainer = IsLastInContainer; - _childrenDepth--; + var children = containerBlock; + for (int i = 0; i < children.Count; i++) + { + IsFirstInContainer = i == 0; + IsLastInContainer = i + 1 == children.Count; + Write(children[i]); } - /// - /// Writes the children of the specified . - /// - /// The container inline. - public void WriteChildren(ContainerInline containerInline) - { - if (containerInline is null) - { - return; - } + IsFirstInContainer = saveIsFirstInContainer; + IsLastInContainer = saveIsLastInContainer; - ThrowHelper.CheckDepthLimit(_childrenDepth++); + _childrenDepth--; + } - bool saveIsFirstInContainer = IsFirstInContainer; - bool saveIsLastInContainer = IsLastInContainer; + /// + /// Writes the children of the specified . + /// + /// The container inline. + public void WriteChildren(ContainerInline containerInline) + { + if (containerInline is null) + { + return; + } - bool isFirst = true; - var inline = containerInline.FirstChild; - while (inline != null) - { - IsFirstInContainer = isFirst; - IsLastInContainer = inline.NextSibling is null; + ThrowHelper.CheckDepthLimit(_childrenDepth++); - Write(inline); - inline = inline.NextSibling; + bool saveIsFirstInContainer = IsFirstInContainer; + bool saveIsLastInContainer = IsLastInContainer; - isFirst = false; - } + bool isFirst = true; + var inline = containerInline.FirstChild; + while (inline != null) + { + IsFirstInContainer = isFirst; + IsLastInContainer = inline.NextSibling is null; - IsFirstInContainer = saveIsFirstInContainer; - IsLastInContainer = saveIsLastInContainer; + Write(inline); + inline = inline.NextSibling; - _childrenDepth--; + isFirst = false; } - /// - /// Writes the specified Markdown object. - /// - /// The Markdown object to write to this renderer. - public void Write(MarkdownObject obj) - { - if (obj is null) - { - return; - } + IsFirstInContainer = saveIsFirstInContainer; + IsLastInContainer = saveIsLastInContainer; - // Calls before writing an object - ObjectWriteBefore?.Invoke(this, obj); + _childrenDepth--; + } - if (!_renderersPerType.TryGetValue(GetKeyForType(obj), out IMarkdownObjectRenderer? renderer)) - { - renderer = GetRendererInstance(obj); - } + /// + /// Writes the specified Markdown object. + /// + /// The Markdown object to write to this renderer. + public void Write(MarkdownObject obj) + { + if (obj is null) + { + return; + } - if (renderer is not null) - { - renderer.Write(this, obj); - } - else if (obj.IsContainerInline) - { - WriteChildren(Unsafe.As(obj)); - } - else if (obj.IsContainerBlock) - { - WriteChildren(Unsafe.As(obj)); - } + // Calls before writing an object + ObjectWriteBefore?.Invoke(this, obj); - // Calls after writing an object - ObjectWriteAfter?.Invoke(this, obj); + if (!_renderersPerType.TryGetValue(GetKeyForType(obj), out IMarkdownObjectRenderer? renderer)) + { + renderer = GetRendererInstance(obj); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static KeyWrapper GetKeyForType(MarkdownObject obj) + if (renderer is not null) { - IntPtr typeHandle = Type.GetTypeHandle(obj).Value; - return new KeyWrapper(typeHandle); + renderer.Write(this, obj); } - - private readonly struct KeyWrapper : IEquatable + else if (obj.IsContainerInline) + { + WriteChildren(Unsafe.As(obj)); + } + else if (obj.IsContainerBlock) { - public readonly IntPtr Key; + WriteChildren(Unsafe.As(obj)); + } - public KeyWrapper(IntPtr key) => Key = key; + // Calls after writing an object + ObjectWriteAfter?.Invoke(this, obj); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static KeyWrapper GetKeyForType(MarkdownObject obj) + { + IntPtr typeHandle = Type.GetTypeHandle(obj).Value; + return new KeyWrapper(typeHandle); + } - public bool Equals(KeyWrapper other) => Key == other.Key; + private readonly struct KeyWrapper : IEquatable + { + public readonly IntPtr Key; - public override int GetHashCode() => Key.GetHashCode(); + public KeyWrapper(IntPtr key) => Key = key; - public override bool Equals(object? obj) => throw new NotImplementedException(); - } + public bool Equals(KeyWrapper other) => Key == other.Key; + + public override int GetHashCode() => Key.GetHashCode(); + + public override bool Equals(object? obj) => throw new NotImplementedException(); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs b/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs index 5f5698e0d..50d7dd9b6 100644 --- a/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs @@ -4,97 +4,95 @@ using Markdig.Helpers; using Markdig.Syntax; -using System.Collections.Generic; -namespace Markdig.Renderers.Roundtrip +namespace Markdig.Renderers.Roundtrip; + +/// +/// An Roundtrip renderer for a and . +/// +/// +public class CodeBlockRenderer : RoundtripObjectRenderer { - /// - /// An Roundtrip renderer for a and . - /// - /// - public class CodeBlockRenderer : RoundtripObjectRenderer + protected override void Write(RoundtripRenderer renderer, CodeBlock obj) { - protected override void Write(RoundtripRenderer renderer, CodeBlock obj) + renderer.RenderLinesBefore(obj); + if (obj is FencedCodeBlock fencedCodeBlock) { - renderer.RenderLinesBefore(obj); - if (obj is FencedCodeBlock fencedCodeBlock) - { - renderer.Write(obj.TriviaBefore); - var opening = new string(fencedCodeBlock.FencedChar, fencedCodeBlock.OpeningFencedCharCount); - renderer.Write(opening); + renderer.Write(obj.TriviaBefore); + var opening = new string(fencedCodeBlock.FencedChar, fencedCodeBlock.OpeningFencedCharCount); + renderer.Write(opening); - if (!fencedCodeBlock.TriviaAfterFencedChar.IsEmpty) - { - renderer.Write(fencedCodeBlock.TriviaAfterFencedChar); - } - if (fencedCodeBlock.Info != null) - { - renderer.Write(fencedCodeBlock.UnescapedInfo); - } - if (!fencedCodeBlock.TriviaAfterInfo.IsEmpty) - { - renderer.Write(fencedCodeBlock.TriviaAfterInfo); - } - if (!string.IsNullOrEmpty(fencedCodeBlock.Arguments)) - { - renderer.Write(fencedCodeBlock.UnescapedArguments); - } - if (!fencedCodeBlock.TriviaAfterArguments.IsEmpty) - { - renderer.Write(fencedCodeBlock.TriviaAfterArguments); - } + if (!fencedCodeBlock.TriviaAfterFencedChar.IsEmpty) + { + renderer.Write(fencedCodeBlock.TriviaAfterFencedChar); + } + if (fencedCodeBlock.Info != null) + { + renderer.Write(fencedCodeBlock.UnescapedInfo); + } + if (!fencedCodeBlock.TriviaAfterInfo.IsEmpty) + { + renderer.Write(fencedCodeBlock.TriviaAfterInfo); + } + if (!string.IsNullOrEmpty(fencedCodeBlock.Arguments)) + { + renderer.Write(fencedCodeBlock.UnescapedArguments); + } + if (!fencedCodeBlock.TriviaAfterArguments.IsEmpty) + { + renderer.Write(fencedCodeBlock.TriviaAfterArguments); + } - /* TODO do we need this causes a empty space and would render html attributes to markdown. - var attributes = obj.TryGetAttributes(); - if (attributes != null) - { - renderer.Write(" "); - renderer.Write(attributes); - } - */ - renderer.WriteLine(fencedCodeBlock.InfoNewLine); + /* TODO do we need this causes a empty space and would render html attributes to markdown. + var attributes = obj.TryGetAttributes(); + if (attributes != null) + { + renderer.Write(" "); + renderer.Write(attributes); + } + */ + renderer.WriteLine(fencedCodeBlock.InfoNewLine); - renderer.WriteLeafRawLines(obj); + renderer.WriteLeafRawLines(obj); - renderer.Write(fencedCodeBlock.TriviaBeforeClosingFence); - var closing = new string(fencedCodeBlock.FencedChar, fencedCodeBlock.ClosingFencedCharCount); - renderer.Write(closing); - if (!string.IsNullOrEmpty(closing)) - { - // See example 207: "> ```\nfoo\n```" - renderer.WriteLine(obj.NewLine); - } - renderer.Write(obj.TriviaAfter); + renderer.Write(fencedCodeBlock.TriviaBeforeClosingFence); + var closing = new string(fencedCodeBlock.FencedChar, fencedCodeBlock.ClosingFencedCharCount); + renderer.Write(closing); + if (!string.IsNullOrEmpty(closing)) + { + // See example 207: "> ```\nfoo\n```" + renderer.WriteLine(obj.NewLine); } - else + renderer.Write(obj.TriviaAfter); + } + else + { + var indents = new string[obj.CodeBlockLines.Count]; + for (int i = 0; i < obj.CodeBlockLines.Count; i++) { - var indents = new string[obj.CodeBlockLines.Count]; - for (int i = 0; i < obj.CodeBlockLines.Count; i++) - { - indents[i] = obj.CodeBlockLines[i].TriviaBefore.ToString(); - } - renderer.PushIndent(indents); - WriteLeafRawLines(renderer, obj); - renderer.PopIndent(); - - // ignore block newline, as last line references it + indents[i] = obj.CodeBlockLines[i].TriviaBefore.ToString(); } - - renderer.RenderLinesAfter(obj); + renderer.PushIndent(indents); + WriteLeafRawLines(renderer, obj); + renderer.PopIndent(); + + // ignore block newline, as last line references it } - public void WriteLeafRawLines(RoundtripRenderer renderer, LeafBlock leafBlock) + renderer.RenderLinesAfter(obj); + } + + public void WriteLeafRawLines(RoundtripRenderer renderer, LeafBlock leafBlock) + { + if (leafBlock.Lines.Lines != null) { - if (leafBlock.Lines.Lines != null) + var lines = leafBlock.Lines; + var slices = lines.Lines; + for (int i = 0; i < lines.Count; i++) { - var lines = leafBlock.Lines; - var slices = lines.Lines; - for (int i = 0; i < lines.Count; i++) - { - ref StringSlice slice = ref slices[i].Slice; - renderer.Write(ref slice); - renderer.WriteLine(slice.NewLine); - } + ref StringSlice slice = ref slices[i].Slice; + renderer.Write(ref slice); + renderer.WriteLine(slice.NewLine); } } } diff --git a/src/Markdig/Renderers/Roundtrip/EmptyBlockRenderer.cs b/src/Markdig/Renderers/Roundtrip/EmptyBlockRenderer.cs index bd4e2b1c2..9bd5058f0 100644 --- a/src/Markdig/Renderers/Roundtrip/EmptyBlockRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/EmptyBlockRenderer.cs @@ -4,13 +4,12 @@ using Markdig.Syntax; -namespace Markdig.Renderers.Roundtrip +namespace Markdig.Renderers.Roundtrip; + +public class EmptyBlockRenderer : RoundtripObjectRenderer { - public class EmptyBlockRenderer : RoundtripObjectRenderer + protected override void Write(RoundtripRenderer renderer, EmptyBlock noBlocksFoundBlock) { - protected override void Write(RoundtripRenderer renderer, EmptyBlock noBlocksFoundBlock) - { - renderer.RenderLinesAfter(noBlocksFoundBlock); - } + renderer.RenderLinesAfter(noBlocksFoundBlock); } } diff --git a/src/Markdig/Renderers/Roundtrip/HeadingRenderer.cs b/src/Markdig/Renderers/Roundtrip/HeadingRenderer.cs index 0f40d152a..4bef06872 100644 --- a/src/Markdig/Renderers/Roundtrip/HeadingRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/HeadingRenderer.cs @@ -4,58 +4,57 @@ using Markdig.Syntax; -namespace Markdig.Renderers.Roundtrip +namespace Markdig.Renderers.Roundtrip; + +/// +/// An Roundtrip renderer for a . +/// +/// +public class HeadingRenderer : RoundtripObjectRenderer { - /// - /// An Roundtrip renderer for a . - /// - /// - public class HeadingRenderer : RoundtripObjectRenderer + private static readonly string[] HeadingTexts = { + "#", + "##", + "###", + "####", + "#####", + "######", + }; + + protected override void Write(RoundtripRenderer renderer, HeadingBlock obj) { - private static readonly string[] HeadingTexts = { - "#", - "##", - "###", - "####", - "#####", - "######", - }; - - protected override void Write(RoundtripRenderer renderer, HeadingBlock obj) + if (obj.IsSetext) { - if (obj.IsSetext) - { - renderer.RenderLinesBefore(obj); - - var headingChar = obj.Level == 1 ? '=' : '-'; - var line = new string(headingChar, obj.HeaderCharCount); - - renderer.WriteLeafInline(obj); - renderer.WriteLine(obj.SetextNewline); - renderer.Write(obj.TriviaBefore); - renderer.Write(line); - renderer.WriteLine(obj.NewLine); - renderer.Write(obj.TriviaAfter); - - renderer.RenderLinesAfter(obj); - } - else - { - renderer.RenderLinesBefore(obj); - - var headingText = obj.Level > 0 && obj.Level <= 6 - ? HeadingTexts[obj.Level - 1] - : new string('#', obj.Level); - - renderer.Write(obj.TriviaBefore); - renderer.Write(headingText); - renderer.Write(obj.TriviaAfterAtxHeaderChar); - renderer.WriteLeafInline(obj); - renderer.Write(obj.TriviaAfter); - renderer.WriteLine(obj.NewLine); - - renderer.RenderLinesAfter(obj); - } + renderer.RenderLinesBefore(obj); + + var headingChar = obj.Level == 1 ? '=' : '-'; + var line = new string(headingChar, obj.HeaderCharCount); + + renderer.WriteLeafInline(obj); + renderer.WriteLine(obj.SetextNewline); + renderer.Write(obj.TriviaBefore); + renderer.Write(line); + renderer.WriteLine(obj.NewLine); + renderer.Write(obj.TriviaAfter); + + renderer.RenderLinesAfter(obj); + } + else + { + renderer.RenderLinesBefore(obj); + + var headingText = obj.Level > 0 && obj.Level <= 6 + ? HeadingTexts[obj.Level - 1] + : new string('#', obj.Level); + + renderer.Write(obj.TriviaBefore); + renderer.Write(headingText); + renderer.Write(obj.TriviaAfterAtxHeaderChar); + renderer.WriteLeafInline(obj); + renderer.Write(obj.TriviaAfter); + renderer.WriteLine(obj.NewLine); + + renderer.RenderLinesAfter(obj); } } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/HtmlBlockRenderer.cs b/src/Markdig/Renderers/Roundtrip/HtmlBlockRenderer.cs index 07712a7f9..073dfc6e9 100644 --- a/src/Markdig/Renderers/Roundtrip/HtmlBlockRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/HtmlBlockRenderer.cs @@ -4,16 +4,15 @@ using Markdig.Syntax; -namespace Markdig.Renderers.Roundtrip +namespace Markdig.Renderers.Roundtrip; + +public class HtmlBlockRenderer : RoundtripObjectRenderer { - public class HtmlBlockRenderer : RoundtripObjectRenderer + protected override void Write(RoundtripRenderer renderer, HtmlBlock obj) { - protected override void Write(RoundtripRenderer renderer, HtmlBlock obj) - { - renderer.RenderLinesBefore(obj); - //renderer.Write(obj.BeforeWhitespace); // Lines content is written, including whitespace - renderer.WriteLeafRawLines(obj); - renderer.RenderLinesAfter(obj); - } + renderer.RenderLinesBefore(obj); + //renderer.Write(obj.BeforeWhitespace); // Lines content is written, including whitespace + renderer.WriteLeafRawLines(obj); + renderer.RenderLinesAfter(obj); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/Inlines/AutolinkInlineRenderer.cs b/src/Markdig/Renderers/Roundtrip/Inlines/AutolinkInlineRenderer.cs index 4abcf2259..4cbc05f63 100644 --- a/src/Markdig/Renderers/Roundtrip/Inlines/AutolinkInlineRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/Inlines/AutolinkInlineRenderer.cs @@ -4,17 +4,16 @@ using Markdig.Syntax.Inlines; -namespace Markdig.Renderers.Roundtrip.Inlines +namespace Markdig.Renderers.Roundtrip.Inlines; + +/// +/// A Normalize renderer for an . +/// +/// +public class AutolinkInlineRenderer : RoundtripObjectRenderer { - /// - /// A Normalize renderer for an . - /// - /// - public class AutolinkInlineRenderer : RoundtripObjectRenderer + protected override void Write(RoundtripRenderer renderer, AutolinkInline obj) { - protected override void Write(RoundtripRenderer renderer, AutolinkInline obj) - { - renderer.Write('<').Write(obj.Url).Write('>'); - } + renderer.Write('<').Write(obj.Url).Write('>'); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/Inlines/CodeInlineRenderer.cs b/src/Markdig/Renderers/Roundtrip/Inlines/CodeInlineRenderer.cs index be1b9c72e..b4d03a2bb 100644 --- a/src/Markdig/Renderers/Roundtrip/Inlines/CodeInlineRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/Inlines/CodeInlineRenderer.cs @@ -4,23 +4,22 @@ using Markdig.Syntax.Inlines; -namespace Markdig.Renderers.Roundtrip.Inlines +namespace Markdig.Renderers.Roundtrip.Inlines; + +/// +/// A Normalize renderer for a . +/// +/// +public class CodeInlineRenderer : RoundtripObjectRenderer { - /// - /// A Normalize renderer for a . - /// - /// - public class CodeInlineRenderer : RoundtripObjectRenderer + protected override void Write(RoundtripRenderer renderer, CodeInline obj) { - protected override void Write(RoundtripRenderer renderer, CodeInline obj) + var delimiterRun = new string(obj.Delimiter, obj.DelimiterCount); + renderer.Write(delimiterRun); + if (!obj.ContentSpan.IsEmpty) { - var delimiterRun = new string(obj.Delimiter, obj.DelimiterCount); - renderer.Write(delimiterRun); - if (!obj.ContentSpan.IsEmpty) - { - renderer.Write(obj.ContentWithTrivia); - } - renderer.Write(delimiterRun); + renderer.Write(obj.ContentWithTrivia); } + renderer.Write(delimiterRun); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/Inlines/DelimiterInlineRenderer.cs b/src/Markdig/Renderers/Roundtrip/Inlines/DelimiterInlineRenderer.cs index fd12b2783..70fbf9b1c 100644 --- a/src/Markdig/Renderers/Roundtrip/Inlines/DelimiterInlineRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/Inlines/DelimiterInlineRenderer.cs @@ -4,18 +4,17 @@ using Markdig.Syntax.Inlines; -namespace Markdig.Renderers.Roundtrip.Inlines +namespace Markdig.Renderers.Roundtrip.Inlines; + +/// +/// A Normalize renderer for a . +/// +/// +public class DelimiterInlineRenderer : RoundtripObjectRenderer { - /// - /// A Normalize renderer for a . - /// - /// - public class DelimiterInlineRenderer : RoundtripObjectRenderer + protected override void Write(RoundtripRenderer renderer, DelimiterInline obj) { - protected override void Write(RoundtripRenderer renderer, DelimiterInline obj) - { - renderer.Write(obj.ToLiteral()); - renderer.WriteChildren(obj); - } + renderer.Write(obj.ToLiteral()); + renderer.WriteChildren(obj); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/Inlines/EmphasisInlineRenderer.cs b/src/Markdig/Renderers/Roundtrip/Inlines/EmphasisInlineRenderer.cs index a93aa21d6..5e61801ef 100644 --- a/src/Markdig/Renderers/Roundtrip/Inlines/EmphasisInlineRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/Inlines/EmphasisInlineRenderer.cs @@ -4,20 +4,19 @@ using Markdig.Syntax.Inlines; -namespace Markdig.Renderers.Roundtrip.Inlines +namespace Markdig.Renderers.Roundtrip.Inlines; + +/// +/// A Normalize renderer for an . +/// +/// +public class EmphasisInlineRenderer : RoundtripObjectRenderer { - /// - /// A Normalize renderer for an . - /// - /// - public class EmphasisInlineRenderer : RoundtripObjectRenderer + protected override void Write(RoundtripRenderer renderer, EmphasisInline obj) { - protected override void Write(RoundtripRenderer renderer, EmphasisInline obj) - { - var emphasisText = new string(obj.DelimiterChar, obj.DelimiterCount); - renderer.Write(emphasisText); - renderer.WriteChildren(obj); - renderer.Write(emphasisText); - } + var emphasisText = new string(obj.DelimiterChar, obj.DelimiterCount); + renderer.Write(emphasisText); + renderer.WriteChildren(obj); + renderer.Write(emphasisText); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/Inlines/LineBreakInlineRenderer.cs b/src/Markdig/Renderers/Roundtrip/Inlines/LineBreakInlineRenderer.cs index cf8ca8382..ed0b3aa68 100644 --- a/src/Markdig/Renderers/Roundtrip/Inlines/LineBreakInlineRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/Inlines/LineBreakInlineRenderer.cs @@ -4,21 +4,20 @@ using Markdig.Syntax.Inlines; -namespace Markdig.Renderers.Roundtrip.Inlines +namespace Markdig.Renderers.Roundtrip.Inlines; + +/// +/// A Normalize renderer for a . +/// +/// +public class LineBreakInlineRenderer : RoundtripObjectRenderer { - /// - /// A Normalize renderer for a . - /// - /// - public class LineBreakInlineRenderer : RoundtripObjectRenderer + protected override void Write(RoundtripRenderer renderer, LineBreakInline obj) { - protected override void Write(RoundtripRenderer renderer, LineBreakInline obj) + if (obj.IsHard && obj.IsBackslash) { - if (obj.IsHard && obj.IsBackslash) - { - renderer.Write("\\"); - } - renderer.WriteLine(obj.NewLine); + renderer.Write("\\"); } + renderer.WriteLine(obj.NewLine); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/Inlines/LinkInlineRenderer.cs b/src/Markdig/Renderers/Roundtrip/Inlines/LinkInlineRenderer.cs index c388a5920..fa4bd9b00 100644 --- a/src/Markdig/Renderers/Roundtrip/Inlines/LinkInlineRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/Inlines/LinkInlineRenderer.cs @@ -4,70 +4,69 @@ using Markdig.Syntax.Inlines; -namespace Markdig.Renderers.Roundtrip.Inlines +namespace Markdig.Renderers.Roundtrip.Inlines; + +/// +/// A Normalize renderer for a . +/// +/// +public class LinkInlineRenderer : RoundtripObjectRenderer { - /// - /// A Normalize renderer for a . - /// - /// - public class LinkInlineRenderer : RoundtripObjectRenderer + protected override void Write(RoundtripRenderer renderer, LinkInline link) { - protected override void Write(RoundtripRenderer renderer, LinkInline link) + if (link.IsImage) { - if (link.IsImage) - { - renderer.Write('!'); - } - // link text - renderer.Write('['); - renderer.WriteChildren(link); - renderer.Write(']'); + renderer.Write('!'); + } + // link text + renderer.Write('['); + renderer.WriteChildren(link); + renderer.Write(']'); - if (link.Label != null) + if (link.Label != null) + { + if (link.LocalLabel == LocalLabel.Local || link.LocalLabel == LocalLabel.Empty) { - if (link.LocalLabel == LocalLabel.Local || link.LocalLabel == LocalLabel.Empty) + renderer.Write('['); + if (link.LocalLabel == LocalLabel.Local) { - renderer.Write('['); - if (link.LocalLabel == LocalLabel.Local) - { - renderer.Write(link.LabelWithTrivia); - } - renderer.Write(']'); + renderer.Write(link.LabelWithTrivia); } + renderer.Write(']'); } - else + } + else + { + if (link.Url != null) { - if (link.Url != null) + renderer.Write('('); + renderer.Write(link.TriviaBeforeUrl); + if (link.UrlHasPointyBrackets) { - renderer.Write('('); - renderer.Write(link.TriviaBeforeUrl); - if (link.UrlHasPointyBrackets) - { - renderer.Write('<'); - } - renderer.Write(link.UnescapedUrl); - if (link.UrlHasPointyBrackets) - { - renderer.Write('>'); - } - renderer.Write(link.TriviaAfterUrl); + renderer.Write('<'); + } + renderer.Write(link.UnescapedUrl); + if (link.UrlHasPointyBrackets) + { + renderer.Write('>'); + } + renderer.Write(link.TriviaAfterUrl); - if (!string.IsNullOrEmpty(link.Title)) + if (!string.IsNullOrEmpty(link.Title)) + { + var open = link.TitleEnclosingCharacter; + var close = link.TitleEnclosingCharacter; + if (link.TitleEnclosingCharacter == '(') { - var open = link.TitleEnclosingCharacter; - var close = link.TitleEnclosingCharacter; - if (link.TitleEnclosingCharacter == '(') - { - close = ')'; - } - renderer.Write(open); - renderer.Write(link.UnescapedTitle); - renderer.Write(close); - renderer.Write(link.TriviaAfterTitle); + close = ')'; } - - renderer.Write(')'); + renderer.Write(open); + renderer.Write(link.UnescapedTitle); + renderer.Write(close); + renderer.Write(link.TriviaAfterTitle); } + + renderer.Write(')'); } } } diff --git a/src/Markdig/Renderers/Roundtrip/Inlines/LiteralInlineRenderer.cs b/src/Markdig/Renderers/Roundtrip/Inlines/LiteralInlineRenderer.cs index 632ebe874..104e36593 100644 --- a/src/Markdig/Renderers/Roundtrip/Inlines/LiteralInlineRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/Inlines/LiteralInlineRenderer.cs @@ -5,21 +5,20 @@ using Markdig.Helpers; using Markdig.Syntax.Inlines; -namespace Markdig.Renderers.Roundtrip.Inlines +namespace Markdig.Renderers.Roundtrip.Inlines; + +/// +/// A Normalize renderer for a . +/// +/// +public class LiteralInlineRenderer : RoundtripObjectRenderer { - /// - /// A Normalize renderer for a . - /// - /// - public class LiteralInlineRenderer : RoundtripObjectRenderer + protected override void Write(RoundtripRenderer renderer, LiteralInline obj) { - protected override void Write(RoundtripRenderer renderer, LiteralInline obj) + if (obj.IsFirstCharacterEscaped && obj.Content.Length > 0 && obj.Content[obj.Content.Start].IsAsciiPunctuation()) { - if (obj.IsFirstCharacterEscaped && obj.Content.Length > 0 && obj.Content[obj.Content.Start].IsAsciiPunctuation()) - { - renderer.Write('\\'); - } - renderer.Write(ref obj.Content); + renderer.Write('\\'); } + renderer.Write(ref obj.Content); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/Inlines/RoundtripHtmlEntityInlineRenderer.cs b/src/Markdig/Renderers/Roundtrip/Inlines/RoundtripHtmlEntityInlineRenderer.cs index 22fcd9900..cbb51cfea 100644 --- a/src/Markdig/Renderers/Roundtrip/Inlines/RoundtripHtmlEntityInlineRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/Inlines/RoundtripHtmlEntityInlineRenderer.cs @@ -4,16 +4,15 @@ using Markdig.Syntax.Inlines; -namespace Markdig.Renderers.Roundtrip.Inlines +namespace Markdig.Renderers.Roundtrip.Inlines; + +/// +/// A Normalize renderer for a . +/// +public class RoundtripHtmlEntityInlineRenderer : RoundtripObjectRenderer { - /// - /// A Normalize renderer for a . - /// - public class RoundtripHtmlEntityInlineRenderer : RoundtripObjectRenderer + protected override void Write(RoundtripRenderer renderer, HtmlEntityInline obj) { - protected override void Write(RoundtripRenderer renderer, HtmlEntityInline obj) - { - renderer.Write(obj.Original); - } + renderer.Write(obj.Original); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/Inlines/RoundtripHtmlInlineRenderer.cs b/src/Markdig/Renderers/Roundtrip/Inlines/RoundtripHtmlInlineRenderer.cs index 75548d869..d8a161e72 100644 --- a/src/Markdig/Renderers/Roundtrip/Inlines/RoundtripHtmlInlineRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/Inlines/RoundtripHtmlInlineRenderer.cs @@ -4,16 +4,15 @@ using Markdig.Syntax.Inlines; -namespace Markdig.Renderers.Roundtrip.Inlines +namespace Markdig.Renderers.Roundtrip.Inlines; + +/// +/// A Normalize renderer for a . +/// +public class RoundtripHtmlInlineRenderer : RoundtripObjectRenderer { - /// - /// A Normalize renderer for a . - /// - public class RoundtripHtmlInlineRenderer : RoundtripObjectRenderer + protected override void Write(RoundtripRenderer renderer, HtmlInline obj) { - protected override void Write(RoundtripRenderer renderer, HtmlInline obj) - { - renderer.Write(obj.Tag); - } + renderer.Write(obj.Tag); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionGroupRenderer.cs b/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionGroupRenderer.cs index 3aa1c07da..437f8ee3b 100644 --- a/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionGroupRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionGroupRenderer.cs @@ -4,13 +4,12 @@ using Markdig.Syntax; -namespace Markdig.Renderers.Roundtrip +namespace Markdig.Renderers.Roundtrip; + +public class LinkReferenceDefinitionGroupRenderer : RoundtripObjectRenderer { - public class LinkReferenceDefinitionGroupRenderer : RoundtripObjectRenderer + protected override void Write(RoundtripRenderer renderer, LinkReferenceDefinitionGroup obj) { - protected override void Write(RoundtripRenderer renderer, LinkReferenceDefinitionGroup obj) - { - renderer.WriteChildren(obj); - } + renderer.WriteChildren(obj); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionRenderer.cs b/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionRenderer.cs index 9e7c26931..e396f2b1e 100644 --- a/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionRenderer.cs @@ -5,47 +5,46 @@ using Markdig.Helpers; using Markdig.Syntax; -namespace Markdig.Renderers.Roundtrip +namespace Markdig.Renderers.Roundtrip; + +public class LinkReferenceDefinitionRenderer : RoundtripObjectRenderer { - public class LinkReferenceDefinitionRenderer : RoundtripObjectRenderer + protected override void Write(RoundtripRenderer renderer, LinkReferenceDefinition linkDef) { - protected override void Write(RoundtripRenderer renderer, LinkReferenceDefinition linkDef) - { - renderer.RenderLinesBefore(linkDef); + renderer.RenderLinesBefore(linkDef); - renderer.Write(linkDef.TriviaBefore); - renderer.Write('['); - renderer.Write(linkDef.LabelWithTrivia); - renderer.Write("]:"); + renderer.Write(linkDef.TriviaBefore); + renderer.Write('['); + renderer.Write(linkDef.LabelWithTrivia); + renderer.Write("]:"); - renderer.Write(linkDef.TriviaBeforeUrl); - if (linkDef.UrlHasPointyBrackets) - { - renderer.Write('<'); - } - renderer.Write(linkDef.UnescapedUrl); - if (linkDef.UrlHasPointyBrackets) - { - renderer.Write('>'); - } + renderer.Write(linkDef.TriviaBeforeUrl); + if (linkDef.UrlHasPointyBrackets) + { + renderer.Write('<'); + } + renderer.Write(linkDef.UnescapedUrl); + if (linkDef.UrlHasPointyBrackets) + { + renderer.Write('>'); + } - renderer.Write(linkDef.TriviaBeforeTitle); - if (linkDef.Title != null) + renderer.Write(linkDef.TriviaBeforeTitle); + if (linkDef.Title != null) + { + var open = linkDef.TitleEnclosingCharacter; + var close = linkDef.TitleEnclosingCharacter; + if (linkDef.TitleEnclosingCharacter == '(') { - var open = linkDef.TitleEnclosingCharacter; - var close = linkDef.TitleEnclosingCharacter; - if (linkDef.TitleEnclosingCharacter == '(') - { - close = ')'; - } - renderer.Write(open); - renderer.Write(linkDef.UnescapedTitle); - renderer.Write(close); + close = ')'; } - renderer.Write(linkDef.TriviaAfter); - renderer.Write(linkDef.NewLine.AsString()); - - renderer.RenderLinesAfter(linkDef); + renderer.Write(open); + renderer.Write(linkDef.UnescapedTitle); + renderer.Write(close); } + renderer.Write(linkDef.TriviaAfter); + renderer.Write(linkDef.NewLine.AsString()); + + renderer.RenderLinesAfter(linkDef); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/ListRenderer.cs b/src/Markdig/Renderers/Roundtrip/ListRenderer.cs index 759cd8098..41a941281 100644 --- a/src/Markdig/Renderers/Roundtrip/ListRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/ListRenderer.cs @@ -5,61 +5,60 @@ using Markdig.Helpers; using Markdig.Syntax; -namespace Markdig.Renderers.Roundtrip +namespace Markdig.Renderers.Roundtrip; + +/// +/// A Roundtrip renderer for a . +/// +/// +public class ListRenderer : RoundtripObjectRenderer { - /// - /// A Roundtrip renderer for a . - /// - /// - public class ListRenderer : RoundtripObjectRenderer + protected override void Write(RoundtripRenderer renderer, ListBlock listBlock) { - protected override void Write(RoundtripRenderer renderer, ListBlock listBlock) + renderer.RenderLinesBefore(listBlock); + if (listBlock.IsOrdered) { - renderer.RenderLinesBefore(listBlock); - if (listBlock.IsOrdered) + for (var i = 0; i < listBlock.Count; i++) { - for (var i = 0; i < listBlock.Count; i++) - { - var item = listBlock[i]; - var listItem = (ListItemBlock) item; - renderer.RenderLinesBefore(listItem); + var item = listBlock[i]; + var listItem = (ListItemBlock) item; + renderer.RenderLinesBefore(listItem); - var bws = listItem.TriviaBefore.ToString(); - var bullet = listItem.SourceBullet.ToString(); - var delimiter = listBlock.OrderedDelimiter; - renderer.PushIndent(new string[] { $"{bws}{bullet}{delimiter}" }); - renderer.WriteChildren(listItem); - renderer.RenderLinesAfter(listItem); - } + var bws = listItem.TriviaBefore.ToString(); + var bullet = listItem.SourceBullet.ToString(); + var delimiter = listBlock.OrderedDelimiter; + renderer.PushIndent(new string[] { $"{bws}{bullet}{delimiter}" }); + renderer.WriteChildren(listItem); + renderer.RenderLinesAfter(listItem); } - else + } + else + { + for (var i = 0; i < listBlock.Count; i++) { - for (var i = 0; i < listBlock.Count; i++) - { - var item = listBlock[i]; - var listItem = (ListItemBlock) item; - renderer.RenderLinesBefore(listItem); - - StringSlice bws = listItem.TriviaBefore; - char bullet = listBlock.BulletType; - StringSlice aws = listItem.TriviaAfter; + var item = listBlock[i]; + var listItem = (ListItemBlock) item; + renderer.RenderLinesBefore(listItem); - renderer.PushIndent(new string[] { $"{bws}{bullet}{aws}" }); - if (listItem.Count == 0) - { - renderer.Write(""); // trigger writing of indent - } - else - { - renderer.WriteChildren(listItem); - } - renderer.PopIndent(); + StringSlice bws = listItem.TriviaBefore; + char bullet = listBlock.BulletType; + StringSlice aws = listItem.TriviaAfter; - renderer.RenderLinesAfter(listItem); + renderer.PushIndent(new string[] { $"{bws}{bullet}{aws}" }); + if (listItem.Count == 0) + { + renderer.Write(""); // trigger writing of indent } - } + else + { + renderer.WriteChildren(listItem); + } + renderer.PopIndent(); - renderer.RenderLinesAfter(listBlock); + renderer.RenderLinesAfter(listItem); + } } + + renderer.RenderLinesAfter(listBlock); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/ParagraphRenderer.cs b/src/Markdig/Renderers/Roundtrip/ParagraphRenderer.cs index c3b6abfc5..2ac7b098c 100644 --- a/src/Markdig/Renderers/Roundtrip/ParagraphRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/ParagraphRenderer.cs @@ -5,22 +5,21 @@ using Markdig.Syntax; using System.Diagnostics; -namespace Markdig.Renderers.Roundtrip +namespace Markdig.Renderers.Roundtrip; + +/// +/// A Roundtrip renderer for a . +/// +/// +[DebuggerDisplay("renderer.Writer.ToString()")] +public class ParagraphRenderer : RoundtripObjectRenderer { - /// - /// A Roundtrip renderer for a . - /// - /// - [DebuggerDisplay("renderer.Writer.ToString()")] - public class ParagraphRenderer : RoundtripObjectRenderer + protected override void Write(RoundtripRenderer renderer, ParagraphBlock paragraph) { - protected override void Write(RoundtripRenderer renderer, ParagraphBlock paragraph) - { - renderer.RenderLinesBefore(paragraph); - renderer.Write(paragraph.TriviaBefore); - renderer.WriteLeafInline(paragraph); - //renderer.Write(paragraph.Newline); // paragraph typically has LineBreakInlines as closing inline nodes - renderer.RenderLinesAfter(paragraph); - } + renderer.RenderLinesBefore(paragraph); + renderer.Write(paragraph.TriviaBefore); + renderer.WriteLeafInline(paragraph); + //renderer.Write(paragraph.Newline); // paragraph typically has LineBreakInlines as closing inline nodes + renderer.RenderLinesAfter(paragraph); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs b/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs index cca40a883..b146aae9f 100644 --- a/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs @@ -5,60 +5,59 @@ using Markdig.Syntax; using Markdig.Syntax.Inlines; -namespace Markdig.Renderers.Roundtrip +namespace Markdig.Renderers.Roundtrip; + +/// +/// A Roundtrip renderer for a . +/// +public class QuoteBlockRenderer : RoundtripObjectRenderer { - /// - /// A Roundtrip renderer for a . - /// - public class QuoteBlockRenderer : RoundtripObjectRenderer + protected override void Write(RoundtripRenderer renderer, QuoteBlock quoteBlock) { - protected override void Write(RoundtripRenderer renderer, QuoteBlock quoteBlock) - { - renderer.RenderLinesBefore(quoteBlock); - renderer.Write(quoteBlock.TriviaBefore); + renderer.RenderLinesBefore(quoteBlock); + renderer.Write(quoteBlock.TriviaBefore); - var indents = new string[quoteBlock.QuoteLines.Count]; - for (int i = 0; i < quoteBlock.QuoteLines.Count; i++) - { - var quoteLine = quoteBlock.QuoteLines[i]; - var wsb = quoteLine.TriviaBefore.ToString(); - var quoteChar = quoteLine.QuoteChar ? ">" : ""; - var spaceAfterQuoteChar = quoteLine.HasSpaceAfterQuoteChar ? " " : ""; - var wsa = quoteLine.TriviaAfter.ToString(); - indents[i] = (wsb + quoteChar + spaceAfterQuoteChar + wsa); - } - bool noChildren = false; - if (quoteBlock.Count == 0) + var indents = new string[quoteBlock.QuoteLines.Count]; + for (int i = 0; i < quoteBlock.QuoteLines.Count; i++) + { + var quoteLine = quoteBlock.QuoteLines[i]; + var wsb = quoteLine.TriviaBefore.ToString(); + var quoteChar = quoteLine.QuoteChar ? ">" : ""; + var spaceAfterQuoteChar = quoteLine.HasSpaceAfterQuoteChar ? " " : ""; + var wsa = quoteLine.TriviaAfter.ToString(); + indents[i] = (wsb + quoteChar + spaceAfterQuoteChar + wsa); + } + bool noChildren = false; + if (quoteBlock.Count == 0) + { + noChildren = true; + // since this QuoteBlock instance has no children, indents will not be rendered. We + // work around this by adding empty LineBreakInlines to a ParagraphBlock. + // Wanted: a more elegant/better solution (although this is not *that* bad). + foreach (var quoteLine in quoteBlock.QuoteLines) { - noChildren = true; - // since this QuoteBlock instance has no children, indents will not be rendered. We - // work around this by adding empty LineBreakInlines to a ParagraphBlock. - // Wanted: a more elegant/better solution (although this is not *that* bad). - foreach (var quoteLine in quoteBlock.QuoteLines) + var emptyLeafBlock = new ParagraphBlock + { + NewLine = quoteLine.NewLine + }; + var newLine = new LineBreakInline { - var emptyLeafBlock = new ParagraphBlock - { - NewLine = quoteLine.NewLine - }; - var newLine = new LineBreakInline - { - NewLine = quoteLine.NewLine - }; - var container = new ContainerInline(); - container.AppendChild(newLine); - emptyLeafBlock.Inline = container; - quoteBlock.Add(emptyLeafBlock); - } + NewLine = quoteLine.NewLine + }; + var container = new ContainerInline(); + container.AppendChild(newLine); + emptyLeafBlock.Inline = container; + quoteBlock.Add(emptyLeafBlock); } + } - renderer.PushIndent(indents); - renderer.WriteChildren(quoteBlock); - renderer.PopIndent(); + renderer.PushIndent(indents); + renderer.WriteChildren(quoteBlock); + renderer.PopIndent(); - if (!noChildren) - { - renderer.RenderLinesAfter(quoteBlock); - } + if (!noChildren) + { + renderer.RenderLinesAfter(quoteBlock); } } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/RoundtripObjectRenderer.cs b/src/Markdig/Renderers/Roundtrip/RoundtripObjectRenderer.cs index 0f8d7f95c..f55f91c60 100644 --- a/src/Markdig/Renderers/Roundtrip/RoundtripObjectRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/RoundtripObjectRenderer.cs @@ -4,14 +4,13 @@ using Markdig.Syntax; -namespace Markdig.Renderers.Roundtrip +namespace Markdig.Renderers.Roundtrip; + +/// +/// A base class for Normalize rendering and Markdown objects. +/// +/// The type of the object. +/// +public abstract class RoundtripObjectRenderer : MarkdownObjectRenderer where TObject : MarkdownObject { - /// - /// A base class for Normalize rendering and Markdown objects. - /// - /// The type of the object. - /// - public abstract class RoundtripObjectRenderer : MarkdownObjectRenderer where TObject : MarkdownObject - { - } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/RoundtripRenderer.cs b/src/Markdig/Renderers/Roundtrip/RoundtripRenderer.cs index 9e808e194..3ca764482 100644 --- a/src/Markdig/Renderers/Roundtrip/RoundtripRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/RoundtripRenderer.cs @@ -7,91 +7,90 @@ using Markdig.Renderers.Roundtrip.Inlines; using Markdig.Helpers; -namespace Markdig.Renderers.Roundtrip +namespace Markdig.Renderers.Roundtrip; + +/// +/// Markdown renderer honoring trivia for a object. +/// +/// Ensure to call the extension method when +/// parsing markdown to have trivia available for rendering. +public class RoundtripRenderer : TextRendererBase { /// - /// Markdown renderer honoring trivia for a object. + /// Initializes a new instance of the class. /// - /// Ensure to call the extension method when - /// parsing markdown to have trivia available for rendering. - public class RoundtripRenderer : TextRendererBase + /// The writer. + public RoundtripRenderer(TextWriter writer) : base(writer) { - /// - /// Initializes a new instance of the class. - /// - /// The writer. - public RoundtripRenderer(TextWriter writer) : base(writer) - { - // Default block renderers - ObjectRenderers.Add(new CodeBlockRenderer()); - ObjectRenderers.Add(new ListRenderer()); - ObjectRenderers.Add(new HeadingRenderer()); - ObjectRenderers.Add(new HtmlBlockRenderer()); - ObjectRenderers.Add(new ParagraphRenderer()); - ObjectRenderers.Add(new QuoteBlockRenderer()); - ObjectRenderers.Add(new ThematicBreakRenderer()); - ObjectRenderers.Add(new LinkReferenceDefinitionGroupRenderer()); - ObjectRenderers.Add(new LinkReferenceDefinitionRenderer()); - ObjectRenderers.Add(new EmptyBlockRenderer()); + // Default block renderers + ObjectRenderers.Add(new CodeBlockRenderer()); + ObjectRenderers.Add(new ListRenderer()); + ObjectRenderers.Add(new HeadingRenderer()); + ObjectRenderers.Add(new HtmlBlockRenderer()); + ObjectRenderers.Add(new ParagraphRenderer()); + ObjectRenderers.Add(new QuoteBlockRenderer()); + ObjectRenderers.Add(new ThematicBreakRenderer()); + ObjectRenderers.Add(new LinkReferenceDefinitionGroupRenderer()); + ObjectRenderers.Add(new LinkReferenceDefinitionRenderer()); + ObjectRenderers.Add(new EmptyBlockRenderer()); - // Default inline renderers - ObjectRenderers.Add(new AutolinkInlineRenderer()); - ObjectRenderers.Add(new CodeInlineRenderer()); - ObjectRenderers.Add(new DelimiterInlineRenderer()); - ObjectRenderers.Add(new EmphasisInlineRenderer()); - ObjectRenderers.Add(new LineBreakInlineRenderer()); - ObjectRenderers.Add(new RoundtripHtmlInlineRenderer()); - ObjectRenderers.Add(new RoundtripHtmlEntityInlineRenderer()); - ObjectRenderers.Add(new LinkInlineRenderer()); - ObjectRenderers.Add(new LiteralInlineRenderer()); - } + // Default inline renderers + ObjectRenderers.Add(new AutolinkInlineRenderer()); + ObjectRenderers.Add(new CodeInlineRenderer()); + ObjectRenderers.Add(new DelimiterInlineRenderer()); + ObjectRenderers.Add(new EmphasisInlineRenderer()); + ObjectRenderers.Add(new LineBreakInlineRenderer()); + ObjectRenderers.Add(new RoundtripHtmlInlineRenderer()); + ObjectRenderers.Add(new RoundtripHtmlEntityInlineRenderer()); + ObjectRenderers.Add(new LinkInlineRenderer()); + ObjectRenderers.Add(new LiteralInlineRenderer()); + } - /// - /// Writes the lines of a - /// - /// The leaf block. - /// This instance - public void WriteLeafRawLines(LeafBlock leafBlock) + /// + /// Writes the lines of a + /// + /// The leaf block. + /// This instance + public void WriteLeafRawLines(LeafBlock leafBlock) + { + if (leafBlock is null) ThrowHelper.ArgumentNullException_leafBlock(); + if (leafBlock.Lines.Lines != null) { - if (leafBlock is null) ThrowHelper.ArgumentNullException_leafBlock(); - if (leafBlock.Lines.Lines != null) + var lines = leafBlock.Lines; + var slices = lines.Lines; + for (int i = 0; i < lines.Count; i++) { - var lines = leafBlock.Lines; - var slices = lines.Lines; - for (int i = 0; i < lines.Count; i++) - { - var slice = slices[i].Slice; - Write(ref slice); - WriteLine(slice.NewLine); - } + var slice = slices[i].Slice; + Write(ref slice); + WriteLine(slice.NewLine); } } + } - public void RenderLinesBefore(Block block) + public void RenderLinesBefore(Block block) + { + if (block.LinesBefore is null) { - if (block.LinesBefore is null) - { - return; - } - foreach (var line in block.LinesBefore) - { - Write(line); - WriteLine(line.NewLine); - } + return; + } + foreach (var line in block.LinesBefore) + { + Write(line); + WriteLine(line.NewLine); } + } - public void RenderLinesAfter(Block block) + public void RenderLinesAfter(Block block) + { + previousWasLine = true; + if (block.LinesAfter is null) { - previousWasLine = true; - if (block.LinesAfter is null) - { - return; - } - foreach (var line in block.LinesAfter) - { - Write(line); - WriteLine(line.NewLine); - } + return; + } + foreach (var line in block.LinesAfter) + { + Write(line); + WriteLine(line.NewLine); } - } + } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/ThematicBreakRenderer.cs b/src/Markdig/Renderers/Roundtrip/ThematicBreakRenderer.cs index f4f71ab9d..b2868f267 100644 --- a/src/Markdig/Renderers/Roundtrip/ThematicBreakRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/ThematicBreakRenderer.cs @@ -4,21 +4,20 @@ using Markdig.Syntax; -namespace Markdig.Renderers.Roundtrip +namespace Markdig.Renderers.Roundtrip; + +/// +/// A Roundtrip renderer for a . +/// +/// +public class ThematicBreakRenderer : RoundtripObjectRenderer { - /// - /// A Roundtrip renderer for a . - /// - /// - public class ThematicBreakRenderer : RoundtripObjectRenderer + protected override void Write(RoundtripRenderer renderer, ThematicBreakBlock obj) { - protected override void Write(RoundtripRenderer renderer, ThematicBreakBlock obj) - { - renderer.RenderLinesBefore(obj); + renderer.RenderLinesBefore(obj); - renderer.Write(obj.Content); - renderer.WriteLine(obj.NewLine); - renderer.RenderLinesAfter(obj); - } + renderer.Write(obj.Content); + renderer.WriteLine(obj.NewLine); + renderer.RenderLinesAfter(obj); } } \ No newline at end of file diff --git a/src/Markdig/Renderers/TextRendererBase.cs b/src/Markdig/Renderers/TextRendererBase.cs index c5c8c2cc1..ea9b31748 100644 --- a/src/Markdig/Renderers/TextRendererBase.cs +++ b/src/Markdig/Renderers/TextRendererBase.cs @@ -2,383 +2,381 @@ // 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.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.CompilerServices; + using Markdig.Helpers; using Markdig.Syntax; using Markdig.Syntax.Inlines; -namespace Markdig.Renderers +namespace Markdig.Renderers; + +/// +/// A text based . +/// +/// +public abstract class TextRendererBase : RendererBase { + private TextWriter _writer; + /// - /// A text based . + /// Initializes a new instance of the class. /// - /// - public abstract class TextRendererBase : RendererBase + /// The writer. + /// + protected TextRendererBase(TextWriter writer) { - private TextWriter _writer; - - /// - /// Initializes a new instance of the class. - /// - /// The writer. - /// - protected TextRendererBase(TextWriter writer) - { - Writer = writer; - } + Writer = writer; + } - /// - /// Gets or sets the writer. - /// - /// if the value is null - public TextWriter Writer + /// + /// Gets or sets the writer. + /// + /// if the value is null + public TextWriter Writer + { + get => _writer; + [MemberNotNull(nameof(_writer))] + set { - get => _writer; - [MemberNotNull(nameof(_writer))] - set + if (value is null) { - if (value is null) - { - ThrowHelper.ArgumentNullException(nameof(value)); - } - - // By default we output a newline with '\n' only even on Windows platforms - value.NewLine = "\n"; - _writer = value; + ThrowHelper.ArgumentNullException(nameof(value)); } - } - /// - /// Renders the specified markdown object (returns the as a render object). - /// - /// The markdown object. - /// - public override object Render(MarkdownObject markdownObject) - { - Write(markdownObject); - return Writer; + // By default we output a newline with '\n' only even on Windows platforms + value.NewLine = "\n"; + _writer = value; } } /// - /// Typed . + /// Renders the specified markdown object (returns the as a render object). /// - /// Type of the renderer - /// - public abstract class TextRendererBase : TextRendererBase where T : TextRendererBase + /// The markdown object. + /// + public override object Render(MarkdownObject markdownObject) { - private sealed class Indent + Write(markdownObject); + return Writer; + } +} + +/// +/// Typed . +/// +/// Type of the renderer +/// +public abstract class TextRendererBase : TextRendererBase where T : TextRendererBase +{ + private sealed class Indent + { + private readonly string? _constant; + private readonly string[]? _lineSpecific; + private int position; + + internal Indent(string constant) { - private readonly string? _constant; - private readonly string[]? _lineSpecific; - private int position; + _constant = constant; + } - internal Indent(string constant) - { - _constant = constant; - } + internal Indent(string[] lineSpecific) + { + _lineSpecific = lineSpecific; + } - internal Indent(string[] lineSpecific) + internal string Next() + { + if (_constant != null) { - _lineSpecific = lineSpecific; + return _constant; } - internal string Next() - { - if (_constant != null) - { - return _constant; - } - - //if (_lineSpecific.Count == 0) throw new Exception("Indents empty"); - if (position == _lineSpecific!.Length) return string.Empty; + //if (_lineSpecific.Count == 0) throw new Exception("Indents empty"); + if (position == _lineSpecific!.Length) return string.Empty; - return _lineSpecific![position++]; - } + return _lineSpecific![position++]; } + } - protected bool previousWasLine; + protected bool previousWasLine; #if !NETSTANDARD2_1_OR_GREATER && !NETCOREAPP3_1_OR_GREATER - private char[] buffer; + private char[] buffer; #endif - private readonly List indents; + private readonly List indents; - /// - /// Initializes a new instance of the class. - /// - /// The writer. - protected TextRendererBase(TextWriter writer) : base(writer) - { + /// + /// Initializes a new instance of the class. + /// + /// The writer. + protected TextRendererBase(TextWriter writer) : base(writer) + { #if !NETSTANDARD2_1_OR_GREATER && !NETCOREAPP3_1_OR_GREATER - buffer = new char[1024]; + buffer = new char[1024]; #endif - // We assume that we are starting as if we had previously a newline - previousWasLine = true; - indents = new List(); - } + // We assume that we are starting as if we had previously a newline + previousWasLine = true; + indents = new List(); + } - protected internal void Reset() + protected internal void Reset() + { + if (Writer is StringWriter stringWriter) { - if (Writer is StringWriter stringWriter) - { - stringWriter.GetStringBuilder().Length = 0; - } - else - { - ThrowHelper.InvalidOperationException("Cannot reset this TextWriter instance"); - } - - ResetInternal(); + stringWriter.GetStringBuilder().Length = 0; } - - internal void ResetInternal() + else { - _childrenDepth = 0; - previousWasLine = true; - indents.Clear(); + ThrowHelper.InvalidOperationException("Cannot reset this TextWriter instance"); } - /// - /// Ensures a newline. - /// - /// This instance - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public T EnsureLine() - { - if (!previousWasLine) - { - previousWasLine = true; - Writer.WriteLine(); - } - return (T)this; - } + ResetInternal(); + } - public void PushIndent(string indent) - { - if (indent is null) ThrowHelper.ArgumentNullException(nameof(indent)); - indents.Add(new Indent(indent)); - } + internal void ResetInternal() + { + _childrenDepth = 0; + previousWasLine = true; + indents.Clear(); + } - public void PushIndent(string[] lineSpecific) + /// + /// Ensures a newline. + /// + /// This instance + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T EnsureLine() + { + if (!previousWasLine) { - if (indents is null) ThrowHelper.ArgumentNullException(nameof(indents)); - indents.Add(new Indent(lineSpecific)); - - // ensure that indents are written to the output stream - // this assumes that calls after PushIndent wil write children content previousWasLine = true; + Writer.WriteLine(); } + return (T)this; + } - public void PopIndent() - { - // TODO: Check - indents.RemoveAt(indents.Count - 1); - } + public void PushIndent(string indent) + { + if (indent is null) ThrowHelper.ArgumentNullException(nameof(indent)); + indents.Add(new Indent(indent)); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private protected void WriteIndent() - { - if (previousWasLine) - { - WriteIndentCore(); - } - } + public void PushIndent(string[] lineSpecific) + { + if (indents is null) ThrowHelper.ArgumentNullException(nameof(indents)); + indents.Add(new Indent(lineSpecific)); - private void WriteIndentCore() - { - previousWasLine = false; - for (int i = 0; i < indents.Count; i++) - { - var indent = indents[i]; - var indentText = indent.Next(); - Writer.Write(indentText); - } - } + // ensure that indents are written to the output stream + // this assumes that calls after PushIndent wil write children content + previousWasLine = true; + } - /// - /// Writes the specified content. - /// - /// The content. - /// This instance - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public T Write(string? content) - { - WriteIndent(); - Writer.Write(content); - return (T)this; - } + public void PopIndent() + { + // TODO: Check + indents.RemoveAt(indents.Count - 1); + } - /// - /// Writes the specified slice. - /// - /// The slice. - /// This instance - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public T Write(ref StringSlice slice) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private protected void WriteIndent() + { + if (previousWasLine) { - Write(slice.AsSpan()); - return (T)this; + WriteIndentCore(); } + } - /// - /// Writes the specified slice. - /// - /// The slice. - /// This instance - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public T Write(StringSlice slice) + private void WriteIndentCore() + { + previousWasLine = false; + for (int i = 0; i < indents.Count; i++) { - Write(slice.AsSpan()); - return (T)this; + var indent = indents[i]; + var indentText = indent.Next(); + Writer.Write(indentText); } + } + + /// + /// Writes the specified content. + /// + /// The content. + /// This instance + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T Write(string? content) + { + WriteIndent(); + Writer.Write(content); + return (T)this; + } + + /// + /// Writes the specified slice. + /// + /// The slice. + /// This instance + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T Write(ref StringSlice slice) + { + Write(slice.AsSpan()); + return (T)this; + } + + /// + /// Writes the specified slice. + /// + /// The slice. + /// This instance + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T Write(StringSlice slice) + { + Write(slice.AsSpan()); + return (T)this; + } - /// - /// Writes the specified character. - /// - /// The content. - /// This instance - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public T Write(char content) + /// + /// Writes the specified character. + /// + /// The content. + /// This instance + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T Write(char content) + { + WriteIndent(); + if (content == '\n') { - WriteIndent(); - if (content == '\n') - { - previousWasLine = true; - } - Writer.Write(content); - return (T)this; + previousWasLine = true; } + Writer.Write(content); + return (T)this; + } - /// - /// Writes the specified content. - /// - /// The content. - /// The offset. - /// The length. - /// This instance - public T Write(string content, int offset, int length) + /// + /// Writes the specified content. + /// + /// The content. + /// The offset. + /// The length. + /// This instance + public T Write(string content, int offset, int length) + { + if (content is not null) { - if (content is not null) - { - Write(content.AsSpan(offset, length)); - } - return (T)this; + Write(content.AsSpan(offset, length)); } + return (T)this; + } - /// - /// Writes the specified content. - /// - /// The content. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Write(ReadOnlySpan content) + /// + /// Writes the specified content. + /// + /// The content. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(ReadOnlySpan content) + { + if (!content.IsEmpty) { - if (!content.IsEmpty) - { - WriteIndent(); - WriteRaw(content); - } + WriteIndent(); + WriteRaw(content); } + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void WriteRaw(char content) => Writer.Write(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(string? content) => Writer.Write(content); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void WriteRaw(ReadOnlySpan content) - { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void WriteRaw(ReadOnlySpan content) + { #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER - Writer.Write(content); + Writer.Write(content); #else - if (content.Length > buffer.Length) - { - buffer = content.ToArray(); - } - else - { - content.CopyTo(buffer); - } - Writer.Write(buffer, 0, content.Length); -#endif - } - - /// - /// Writes a newline. - /// - /// This instance - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public T WriteLine() + if (content.Length > buffer.Length) { - WriteIndent(); - Writer.WriteLine(); - previousWasLine = true; - return (T)this; + buffer = content.ToArray(); } - - /// - /// Writes a newline. - /// - /// This instance - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public T WriteLine(NewLine newLine) + else { - WriteIndent(); - Writer.Write(newLine.AsString()); - previousWasLine = true; - return (T)this; + content.CopyTo(buffer); } + Writer.Write(buffer, 0, content.Length); +#endif + } - /// - /// Writes a content followed by a newline. - /// - /// The content. - /// This instance - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public T WriteLine(string content) - { - WriteIndent(); - previousWasLine = true; - Writer.WriteLine(content); - return (T)this; - } + /// + /// Writes a newline. + /// + /// This instance + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T WriteLine() + { + WriteIndent(); + Writer.WriteLine(); + previousWasLine = true; + return (T)this; + } - /// - /// Writes a content followed by a newline. - /// - /// The content. - /// This instance - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public T WriteLine(char content) - { - WriteIndent(); - previousWasLine = true; - Writer.WriteLine(content); - return (T)this; - } + /// + /// Writes a newline. + /// + /// This instance + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T WriteLine(NewLine newLine) + { + WriteIndent(); + Writer.Write(newLine.AsString()); + previousWasLine = true; + return (T)this; + } - /// - /// Writes the inlines of a leaf inline. - /// - /// The leaf block. - /// This instance - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public T WriteLeafInline(LeafBlock leafBlock) - { - if (leafBlock is null) ThrowHelper.ArgumentNullException_leafBlock(); - Inline? inline = leafBlock.Inline; + /// + /// Writes a content followed by a newline. + /// + /// The content. + /// This instance + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T WriteLine(string content) + { + WriteIndent(); + previousWasLine = true; + Writer.WriteLine(content); + return (T)this; + } - while (inline != null) - { - Write(inline); - inline = inline.NextSibling; - } + /// + /// Writes a content followed by a newline. + /// + /// The content. + /// This instance + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T WriteLine(char content) + { + WriteIndent(); + previousWasLine = true; + Writer.WriteLine(content); + return (T)this; + } + + /// + /// Writes the inlines of a leaf inline. + /// + /// The leaf block. + /// This instance + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T WriteLeafInline(LeafBlock leafBlock) + { + if (leafBlock is null) ThrowHelper.ArgumentNullException_leafBlock(); + Inline? inline = leafBlock.Inline; - return (T)this; + while (inline != null) + { + Write(inline); + inline = inline.NextSibling; } + + return (T)this; } } \ No newline at end of file diff --git a/src/Markdig/Syntax/BlankLineBlock.cs b/src/Markdig/Syntax/BlankLineBlock.cs index 8771271fb..f4c12428c 100644 --- a/src/Markdig/Syntax/BlankLineBlock.cs +++ b/src/Markdig/Syntax/BlankLineBlock.cs @@ -2,21 +2,20 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -namespace Markdig.Syntax +namespace Markdig.Syntax; + +/// +/// A blank line, used internally by some parsers to store blank lines in a container. They are removed before the end of the document. +/// +/// +public sealed class BlankLineBlock : Block { /// - /// A blank line, used internally by some parsers to store blank lines in a container. They are removed before the end of the document. + /// Initializes a new instance of the class. /// - /// - public sealed class BlankLineBlock : Block + public BlankLineBlock() : base(null) { - /// - /// Initializes a new instance of the class. - /// - public BlankLineBlock() : base(null) - { - // A blankline is never opened - IsOpen = false; - } + // A blankline is never opened + IsOpen = false; } } \ No newline at end of file diff --git a/src/Markdig/Syntax/Block.cs b/src/Markdig/Syntax/Block.cs index f275a6da7..89bea2be5 100644 --- a/src/Markdig/Syntax/Block.cs +++ b/src/Markdig/Syntax/Block.cs @@ -4,163 +4,161 @@ using Markdig.Helpers; using Markdig.Parsers; -using System.Collections.Generic; -namespace Markdig.Syntax +namespace Markdig.Syntax; + +/// +/// Base class for a block structure. Either a or a . +/// +/// +public abstract class Block : MarkdownObject, IBlock { + private BlockTriviaProperties? _trivia => GetTrivia(); + private BlockTriviaProperties Trivia => GetOrSetTrivia(); + /// - /// Base class for a block structure. Either a or a . + /// Initializes a new instance of the class. /// - /// - public abstract class Block : MarkdownObject, IBlock + /// The parser used to create this block. + protected Block(BlockParser? parser) { - private BlockTriviaProperties? _trivia => GetTrivia(); - private BlockTriviaProperties Trivia => GetOrSetTrivia(); - - /// - /// Initializes a new instance of the class. - /// - /// The parser used to create this block. - protected Block(BlockParser? parser) - { - Parser = parser; - IsOpen = true; - IsBreakable = true; - SetTypeKind(isInline: false, isContainer: false); - } + Parser = parser; + IsOpen = true; + IsBreakable = true; + SetTypeKind(isInline: false, isContainer: false); + } - /// - /// Gets the parent of this container. May be null. - /// - public ContainerBlock? Parent { get; internal set; } - - /// - /// Gets the parser associated to this instance. - /// - public BlockParser? Parser { get; } - - internal bool IsLeafBlock { get; private protected set; } - - internal bool IsParagraphBlock { get; private protected set; } - - /// - /// Gets or sets a value indicating whether this instance is still open. - /// - public bool IsOpen { get; set; } - - /// - /// Gets or sets a value indicating whether this block is breakable. Default is true. - /// - public bool IsBreakable { get; set; } - - /// - /// The last newline of this block. - /// Trivia: only parsed when is enabled - /// - public NewLine NewLine { get; set; } - - /// - /// Gets or sets a value indicating whether this block must be removed from its container after inlines have been processed. - /// - public bool RemoveAfterProcessInlines { get; set; } - - /// - /// Gets or sets the trivia right before this block. - /// Trivia: only parsed when is enabled, otherwise - /// . - /// - public StringSlice TriviaBefore { get => _trivia?.TriviaBefore ?? StringSlice.Empty; set => Trivia.TriviaBefore = value; } - - /// - /// Gets or sets trivia occurring after this block. - /// Trivia: only parsed when is enabled, otherwise - /// . - /// - public StringSlice TriviaAfter { get => _trivia?.TriviaAfter ?? StringSlice.Empty; set => Trivia.TriviaAfter = value; } - - /// - /// Gets or sets the empty lines occurring before this block. - /// Trivia: only parsed when is enabled, otherwise null. - /// - public List? LinesBefore { get => _trivia?.LinesBefore; set => Trivia.LinesBefore = value; } - - /// - /// Gets or sets the empty lines occurring after this block. - /// Trivia: only parsed when is enabled, otherwise null. - /// - public List? LinesAfter { get => _trivia?.LinesAfter; set => Trivia.LinesAfter = value; } - - /// - /// Occurs when the process of inlines begin. - /// - public event ProcessInlineDelegate? ProcessInlinesBegin; - - /// - /// Occurs when the process of inlines ends for this instance. - /// - public event ProcessInlineDelegate? ProcessInlinesEnd; - - /// - /// Called when the process of inlines begin. - /// - /// The inline parser state. - internal void OnProcessInlinesBegin(InlineProcessor state) - { - ProcessInlinesBegin?.Invoke(state, null); - } + /// + /// Gets the parent of this container. May be null. + /// + public ContainerBlock? Parent { get; internal set; } - /// - /// Called when the process of inlines ends. - /// - /// The inline parser state. - internal void OnProcessInlinesEnd(InlineProcessor state) - { - ProcessInlinesEnd?.Invoke(state, null); - } + /// + /// Gets the parser associated to this instance. + /// + public BlockParser? Parser { get; } + + internal bool IsLeafBlock { get; private protected set; } + + internal bool IsParagraphBlock { get; private protected set; } + + /// + /// Gets or sets a value indicating whether this instance is still open. + /// + public bool IsOpen { get; set; } + + /// + /// Gets or sets a value indicating whether this block is breakable. Default is true. + /// + public bool IsBreakable { get; set; } + + /// + /// The last newline of this block. + /// Trivia: only parsed when is enabled + /// + public NewLine NewLine { get; set; } + + /// + /// Gets or sets a value indicating whether this block must be removed from its container after inlines have been processed. + /// + public bool RemoveAfterProcessInlines { get; set; } + + /// + /// Gets or sets the trivia right before this block. + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + public StringSlice TriviaBefore { get => _trivia?.TriviaBefore ?? StringSlice.Empty; set => Trivia.TriviaBefore = value; } + + /// + /// Gets or sets trivia occurring after this block. + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + public StringSlice TriviaAfter { get => _trivia?.TriviaAfter ?? StringSlice.Empty; set => Trivia.TriviaAfter = value; } + + /// + /// Gets or sets the empty lines occurring before this block. + /// Trivia: only parsed when is enabled, otherwise null. + /// + public List? LinesBefore { get => _trivia?.LinesBefore; set => Trivia.LinesBefore = value; } + + /// + /// Gets or sets the empty lines occurring after this block. + /// Trivia: only parsed when is enabled, otherwise null. + /// + public List? LinesAfter { get => _trivia?.LinesAfter; set => Trivia.LinesAfter = value; } - public void UpdateSpanEnd(int spanEnd) + /// + /// Occurs when the process of inlines begin. + /// + public event ProcessInlineDelegate? ProcessInlinesBegin; + + /// + /// Occurs when the process of inlines ends for this instance. + /// + public event ProcessInlineDelegate? ProcessInlinesEnd; + + /// + /// Called when the process of inlines begin. + /// + /// The inline parser state. + internal void OnProcessInlinesBegin(InlineProcessor state) + { + ProcessInlinesBegin?.Invoke(state, null); + } + + /// + /// Called when the process of inlines ends. + /// + /// The inline parser state. + internal void OnProcessInlinesEnd(InlineProcessor state) + { + ProcessInlinesEnd?.Invoke(state, null); + } + + public void UpdateSpanEnd(int spanEnd) + { + // Update parent spans + int depth = 0; + var parent = this; + while (parent != null) { - // Update parent spans - int depth = 0; - var parent = this; - while (parent != null) + if (spanEnd > parent.Span.End) { - if (spanEnd > parent.Span.End) - { - parent.Span.End = spanEnd; - } - parent = parent.Parent; - depth++; + parent.Span.End = spanEnd; } - ThrowHelper.CheckDepthLimit(depth, useLargeLimit: true); + parent = parent.Parent; + depth++; } + ThrowHelper.CheckDepthLimit(depth, useLargeLimit: true); + } - internal static Block FindRootMostContainerParent(Block block) + internal static Block FindRootMostContainerParent(Block block) + { + while (true) { - while (true) + Block? parent = block.Parent; + if (parent is null || !parent.IsContainerBlock || parent is MarkdownDocument) { - Block? parent = block.Parent; - if (parent is null || !parent.IsContainerBlock || parent is MarkdownDocument) - { - break; - } - block = parent; + break; } - return block; + block = parent; } + return block; + } - private protected T? TryGetDerivedTrivia() where T : class => _trivia?.DerivedTriviaSlot as T; - private protected T GetOrSetDerivedTrivia() where T : new() => (T)(Trivia.DerivedTriviaSlot ??= new T()); + private protected T? TryGetDerivedTrivia() where T : class => _trivia?.DerivedTriviaSlot as T; + private protected T GetOrSetDerivedTrivia() where T : new() => (T)(Trivia.DerivedTriviaSlot ??= new T()); - private sealed class BlockTriviaProperties - { - // Used by derived types to store their own TriviaProperties - public object? DerivedTriviaSlot; + private sealed class BlockTriviaProperties + { + // Used by derived types to store their own TriviaProperties + public object? DerivedTriviaSlot; - public StringSlice TriviaBefore; - public StringSlice TriviaAfter; - public List? LinesBefore; - public List? LinesAfter; - } + public StringSlice TriviaBefore; + public StringSlice TriviaAfter; + public List? LinesBefore; + public List? LinesAfter; } } \ No newline at end of file diff --git a/src/Markdig/Syntax/BlockExtensions.cs b/src/Markdig/Syntax/BlockExtensions.cs index 13f2a25d1..4e5c77e25 100644 --- a/src/Markdig/Syntax/BlockExtensions.cs +++ b/src/Markdig/Syntax/BlockExtensions.cs @@ -2,135 +2,134 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -namespace Markdig.Syntax +namespace Markdig.Syntax; + +/// +/// Extensions for +/// +public static class BlockExtensions { - /// - /// Extensions for - /// - public static class BlockExtensions - { - // TODO: Add test for this code + // TODO: Add test for this code - public static Block? FindBlockAtPosition(this Block rootBlock, int position) + public static Block? FindBlockAtPosition(this Block rootBlock, int position) + { + var contains = rootBlock.CompareToPosition(position) == 0; + if (!(rootBlock is ContainerBlock blocks) || blocks.Count == 0 || !contains) { - var contains = rootBlock.CompareToPosition(position) == 0; - if (!(rootBlock is ContainerBlock blocks) || blocks.Count == 0 || !contains) - { - return contains ? rootBlock : null; - } - - var lowerIndex = 0; - var upperIndex = blocks.Count - 1; + return contains ? rootBlock : null; + } - // binary search on lines - Block? block = null; - while (lowerIndex <= upperIndex) - { - int midIndex = (upperIndex - lowerIndex) / 2 + lowerIndex; - block = blocks[midIndex]; - int comparison = block.CompareToPosition(position); - if (comparison == 0) - { - break; - } - - block = null; - if (comparison < 0) - lowerIndex = midIndex + 1; - else - upperIndex = midIndex - 1; - } + var lowerIndex = 0; + var upperIndex = blocks.Count - 1; - if (block is null) + // binary search on lines + Block? block = null; + while (lowerIndex <= upperIndex) + { + int midIndex = (upperIndex - lowerIndex) / 2 + lowerIndex; + block = blocks[midIndex]; + int comparison = block.CompareToPosition(position); + if (comparison == 0) { - return rootBlock; + break; } - // Recursively go deep into the block - return FindBlockAtPosition(block, position); + block = null; + if (comparison < 0) + lowerIndex = midIndex + 1; + else + upperIndex = midIndex - 1; } - public static int FindClosestLine(this MarkdownDocument root, int line) + if (block is null) { - var closestBlock = root.FindClosestBlock(line); - return closestBlock?.Line ?? 0; + return rootBlock; } - public static Block? FindClosestBlock(this Block rootBlock, int line) + // Recursively go deep into the block + return FindBlockAtPosition(block, position); + } + + public static int FindClosestLine(this MarkdownDocument root, int line) + { + var closestBlock = root.FindClosestBlock(line); + return closestBlock?.Line ?? 0; + } + + public static Block? FindClosestBlock(this Block rootBlock, int line) + { + if (!(rootBlock is ContainerBlock blocks) || blocks.Count == 0) { - if (!(rootBlock is ContainerBlock blocks) || blocks.Count == 0) - { - return rootBlock.Line == line ? rootBlock : null; - } + return rootBlock.Line == line ? rootBlock : null; + } - var lowerIndex = 0; - var upperIndex = blocks.Count - 1; + var lowerIndex = 0; + var upperIndex = blocks.Count - 1; - // binary search on lines - while (lowerIndex <= upperIndex) + // binary search on lines + while (lowerIndex <= upperIndex) + { + int midIndex = (upperIndex - lowerIndex) / 2 + lowerIndex; + var block = blocks[midIndex]; + int comparison = block.Line.CompareTo(line); + if (comparison == 0) { - int midIndex = (upperIndex - lowerIndex) / 2 + lowerIndex; - var block = blocks[midIndex]; - int comparison = block.Line.CompareTo(line); - if (comparison == 0) - { - return block; - } - if (comparison < 0) - lowerIndex = midIndex + 1; - else - upperIndex = midIndex - 1; + return block; } + if (comparison < 0) + lowerIndex = midIndex + 1; + else + upperIndex = midIndex - 1; + } - // If we are between two lines, try to find the best spot - if (lowerIndex > 0 && lowerIndex < blocks.Count) - { - var prevBlock = blocks[lowerIndex - 1].FindClosestBlock(line) ?? blocks[lowerIndex - 1]; - var nextBlock = blocks[lowerIndex].FindClosestBlock(line) ?? blocks[lowerIndex]; - - if (prevBlock.Line == line) - { - return prevBlock; - } - - if (nextBlock.Line == line) - { - return nextBlock; - } - - // we calculate the position of the current line relative to the line found and previous line - var prevLine = prevBlock.Line; - var nextLine = nextBlock.Line; - - var middle = (line - prevLine) * 1.0 / (nextLine - prevLine); - // If relative position < 0.5, we select the previous line, otherwise we select the line found - return middle < 0.5 ? prevBlock : nextBlock; - } + // If we are between two lines, try to find the best spot + if (lowerIndex > 0 && lowerIndex < blocks.Count) + { + var prevBlock = blocks[lowerIndex - 1].FindClosestBlock(line) ?? blocks[lowerIndex - 1]; + var nextBlock = blocks[lowerIndex].FindClosestBlock(line) ?? blocks[lowerIndex]; - if (lowerIndex == 0) + if (prevBlock.Line == line) { - var prevBlock = blocks[lowerIndex].FindClosestBlock(line) ?? blocks[lowerIndex]; return prevBlock; } - if (lowerIndex == blocks.Count) + if (nextBlock.Line == line) { - var prevBlock = blocks[lowerIndex - 1].FindClosestBlock(line) ?? blocks[lowerIndex - 1]; - return prevBlock; + return nextBlock; } - return null; - } + // we calculate the position of the current line relative to the line found and previous line + var prevLine = prevBlock.Line; + var nextLine = nextBlock.Line; + var middle = (line - prevLine) * 1.0 / (nextLine - prevLine); + // If relative position < 0.5, we select the previous line, otherwise we select the line found + return middle < 0.5 ? prevBlock : nextBlock; + } - public static bool ContainsPosition(this Block block, int position) + if (lowerIndex == 0) { - return CompareToPosition(block, position) == 0; + var prevBlock = blocks[lowerIndex].FindClosestBlock(line) ?? blocks[lowerIndex]; + return prevBlock; } - public static int CompareToPosition(this Block block, int position) + if (lowerIndex == blocks.Count) { - return position < block.Span.Start ? 1 : position > block.Span.End + 1 ? -1 : 0; + var prevBlock = blocks[lowerIndex - 1].FindClosestBlock(line) ?? blocks[lowerIndex - 1]; + return prevBlock; } + + return null; + } + + + public static bool ContainsPosition(this Block block, int position) + { + return CompareToPosition(block, position) == 0; + } + + public static int CompareToPosition(this Block block, int position) + { + return position < block.Span.Start ? 1 : position > block.Span.End + 1 ? -1 : 0; } } \ No newline at end of file diff --git a/src/Markdig/Syntax/CharIteratorHelper.cs b/src/Markdig/Syntax/CharIteratorHelper.cs index de0c2e2b1..ea3f28ee7 100644 --- a/src/Markdig/Syntax/CharIteratorHelper.cs +++ b/src/Markdig/Syntax/CharIteratorHelper.cs @@ -4,52 +4,51 @@ using Markdig.Helpers; -namespace Markdig.Syntax +namespace Markdig.Syntax; + +/// +/// Helpers for the class. +/// +public static class CharIteratorHelper { - /// - /// Helpers for the class. - /// - public static class CharIteratorHelper + public static bool TrimStartAndCountNewLines(ref T iterator, out int countNewLines) where T : ICharIterator { - public static bool TrimStartAndCountNewLines(ref T iterator, out int countNewLines) where T : ICharIterator - { - return TrimStartAndCountNewLines(ref iterator, out countNewLines, out _); - } + return TrimStartAndCountNewLines(ref iterator, out countNewLines, out _); + } - public static bool TrimStartAndCountNewLines(ref T iterator, out int countNewLines, out NewLine lastLine) where T : ICharIterator + public static bool TrimStartAndCountNewLines(ref T iterator, out int countNewLines, out NewLine lastLine) where T : ICharIterator + { + countNewLines = 0; + var c = iterator.CurrentChar; + bool hasWhitespaces = false; + lastLine = NewLine.None; + while (c != '\0' && c.IsWhitespace()) { - countNewLines = 0; - var c = iterator.CurrentChar; - bool hasWhitespaces = false; - lastLine = NewLine.None; - while (c != '\0' && c.IsWhitespace()) + if (c == '\n' || c == '\r') { - if (c == '\n' || c == '\r') + if (c == '\r' && iterator.PeekChar() == '\n') { - if (c == '\r' && iterator.PeekChar() == '\n') - { - lastLine = NewLine.CarriageReturnLineFeed; - iterator.SkipChar(); // skip \n - } - else if (c == '\n') - { - lastLine = NewLine.LineFeed; - } - else if (c == '\r') - { - lastLine = NewLine.CarriageReturn; - } - countNewLines++; + lastLine = NewLine.CarriageReturnLineFeed; + iterator.SkipChar(); // skip \n } - else + else if (c == '\n') { - // reset last line if if have a whitespace after - lastLine = NewLine.None; + lastLine = NewLine.LineFeed; } - hasWhitespaces = true; - c = iterator.NextChar(); + else if (c == '\r') + { + lastLine = NewLine.CarriageReturn; + } + countNewLines++; + } + else + { + // reset last line if if have a whitespace after + lastLine = NewLine.None; } - return hasWhitespaces; + hasWhitespaces = true; + c = iterator.NextChar(); } + return hasWhitespaces; } } \ No newline at end of file diff --git a/src/Markdig/Syntax/CodeBlock.cs b/src/Markdig/Syntax/CodeBlock.cs index 884ac0972..2d82bd061 100644 --- a/src/Markdig/Syntax/CodeBlock.cs +++ b/src/Markdig/Syntax/CodeBlock.cs @@ -4,32 +4,30 @@ using Markdig.Helpers; using Markdig.Parsers; -using System.Collections.Generic; -namespace Markdig.Syntax +namespace Markdig.Syntax; + +/// +/// Represents an indented code block. +/// +/// +/// Related to CommonMark spec: 4.4 Indented code blocks +/// +public class CodeBlock : LeafBlock { - /// - /// Represents an indented code block. - /// - /// - /// Related to CommonMark spec: 4.4 Indented code blocks - /// - public class CodeBlock : LeafBlock + public class CodeBlockLine { - public class CodeBlockLine - { - public StringSlice TriviaBefore { get; set; } - } + public StringSlice TriviaBefore { get; set; } + } - private List? _codeBlockLines; - public List CodeBlockLines => _codeBlockLines ??= new(); + private List? _codeBlockLines; + public List CodeBlockLines => _codeBlockLines ??= new(); - /// - /// Initializes a new instance of the class. - /// - /// The parser. - public CodeBlock(BlockParser parser) : base(parser) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The parser. + public CodeBlock(BlockParser parser) : base(parser) + { } } \ No newline at end of file diff --git a/src/Markdig/Syntax/ContainerBlock.cs b/src/Markdig/Syntax/ContainerBlock.cs index 46143122c..11a9430d7 100644 --- a/src/Markdig/Syntax/ContainerBlock.cs +++ b/src/Markdig/Syntax/ContainerBlock.cs @@ -2,334 +2,332 @@ // 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.Collections; -using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; + using Markdig.Helpers; using Markdig.Parsers; -namespace Markdig.Syntax +namespace Markdig.Syntax; + +/// +/// A base class for container blocks. +/// +/// +[DebuggerDisplay("{GetType().Name} Count = {Count}")] +public abstract class ContainerBlock : Block, IList, IReadOnlyList { + private BlockWrapper[] _children; + /// - /// A base class for container blocks. + /// Initializes a new instance of the class. /// - /// - [DebuggerDisplay("{GetType().Name} Count = {Count}")] - public abstract class ContainerBlock : Block, IList, IReadOnlyList + /// The parser used to create this block. + protected ContainerBlock(BlockParser? parser) : base(parser) { - private BlockWrapper[] _children; - - /// - /// Initializes a new instance of the class. - /// - /// The parser used to create this block. - protected ContainerBlock(BlockParser? parser) : base(parser) - { - _children = Array.Empty(); - SetTypeKind(isInline: false, isContainer: true); - } + _children = Array.Empty(); + SetTypeKind(isInline: false, isContainer: true); + } - /// - /// Gets the last child. - /// - public Block? LastChild + /// + /// Gets the last child. + /// + public Block? LastChild + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get + BlockWrapper[] children = _children; + int index = Count - 1; + if ((uint)index < (uint)children.Length) { - BlockWrapper[] children = _children; - int index = Count - 1; - if ((uint)index < (uint)children.Length) - { - return children[index].Block; - } - else - { - return null; - } + return children[index].Block; + } + else + { + return null; } } + } - /// - /// Specialize enumerator. - /// - /// - public Enumerator GetEnumerator() - { - return new Enumerator(this); - } + /// + /// Specialize enumerator. + /// + /// + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void Add(Block item) + { + if (item is null) + ThrowHelper.ArgumentNullException_item(); - IEnumerator IEnumerable.GetEnumerator() + if (item.Parent != null) { - return GetEnumerator(); + ThrowHelper.ArgumentException("Cannot add this block as it as already attached to another container (block.Parent != null)"); } - public void Add(Block item) + if (Count == _children.Length) { - if (item is null) - ThrowHelper.ArgumentNullException_item(); - - if (item.Parent != null) - { - ThrowHelper.ArgumentException("Cannot add this block as it as already attached to another container (block.Parent != null)"); - } + Grow(); + } + _children[Count] = new BlockWrapper(item); + Count++; + item.Parent = this; - if (Count == _children.Length) - { - Grow(); - } - _children[Count] = new BlockWrapper(item); - Count++; - item.Parent = this; + UpdateSpanEnd(item.Span.End); + } - UpdateSpanEnd(item.Span.End); + private void Grow() + { + if (_children.Length == 0) + { + _children = new BlockWrapper[4]; } - - private void Grow() + else { - if (_children.Length == 0) - { - _children = new BlockWrapper[4]; - } - else - { - Debug.Assert(_children[_children.Length - 1].Block is not null); + Debug.Assert(_children[_children.Length - 1].Block is not null); - var newArray = new BlockWrapper[_children.Length * 2]; - Array.Copy(_children, 0, newArray, 0, Count); - _children = newArray; - } + var newArray = new BlockWrapper[_children.Length * 2]; + Array.Copy(_children, 0, newArray, 0, Count); + _children = newArray; } + } - public void Clear() + public void Clear() + { + BlockWrapper[] children = _children; + for (int i = 0; i < Count && i < children.Length; i++) { - BlockWrapper[] children = _children; - for (int i = 0; i < Count && i < children.Length; i++) - { - children[i].Block.Parent = null; - children[i] = default; - } - Count = 0; + children[i].Block.Parent = null; + children[i] = default; } + Count = 0; + } - public bool Contains(Block item) - { - return IndexOf(item) >= 0; - } + public bool Contains(Block item) + { + return IndexOf(item) >= 0; + } - public void CopyTo(Block[] array, int arrayIndex) + public void CopyTo(Block[] array, int arrayIndex) + { + BlockWrapper[] children = _children; + for (int i = 0; i < Count && i < children.Length; i++) { - BlockWrapper[] children = _children; - for (int i = 0; i < Count && i < children.Length; i++) - { - array[arrayIndex + i] = children[i].Block; - } + array[arrayIndex + i] = children[i].Block; } + } - public bool Remove(Block item) + public bool Remove(Block item) + { + int index = IndexOf(item); + if (index >= 0) { - int index = IndexOf(item); - if (index >= 0) - { - RemoveAt(index); - return true; - } - return false; + RemoveAt(index); + return true; } + return false; + } - public int Count { get; private set; } + public int Count { get; private set; } - public bool IsReadOnly => false; + public bool IsReadOnly => false; - public int IndexOf(Block item) - { - if (item is null) - ThrowHelper.ArgumentNullException_item(); + public int IndexOf(Block item) + { + if (item is null) + ThrowHelper.ArgumentNullException_item(); - BlockWrapper[] children = _children; - for (int i = 0; i < Count && i < children.Length; i++) + BlockWrapper[] children = _children; + for (int i = 0; i < Count && i < children.Length; i++) + { + if (ReferenceEquals(children[i].Block, item)) { - if (ReferenceEquals(children[i].Block, item)) - { - return i; - } + return i; } - return -1; } + return -1; + } - public void Insert(int index, Block item) - { - if (item is null) - ThrowHelper.ArgumentNullException_item(); + public void Insert(int index, Block item) + { + if (item is null) + ThrowHelper.ArgumentNullException_item(); - if (item.Parent != null) - { - ThrowHelper.ArgumentException("Cannot add this block as it as already attached to another container (block.Parent != null)"); - } - if ((uint)index > (uint)Count) - { - ThrowHelper.ArgumentOutOfRangeException_index(); - } - if (Count == _children.Length) - { - Grow(); - } - if (index < Count) - { - Array.Copy(_children, index, _children, index + 1, Count - index); - } - _children[index] = new BlockWrapper(item); - Count++; - item.Parent = this; + if (item.Parent != null) + { + ThrowHelper.ArgumentException("Cannot add this block as it as already attached to another container (block.Parent != null)"); } + if ((uint)index > (uint)Count) + { + ThrowHelper.ArgumentOutOfRangeException_index(); + } + if (Count == _children.Length) + { + Grow(); + } + if (index < Count) + { + Array.Copy(_children, index, _children, index + 1, Count - index); + } + _children[index] = new BlockWrapper(item); + Count++; + item.Parent = this; + } - public void RemoveAt(int index) + public void RemoveAt(int index) + { + if ((uint)index >= (uint)Count) + ThrowHelper.ArgumentOutOfRangeException_index(); + + Count--; + // previous children + var item = _children[index].Block; + item.Parent = null; + if (index < Count) { - if ((uint)index >= (uint)Count) - ThrowHelper.ArgumentOutOfRangeException_index(); - - Count--; - // previous children - var item = _children[index].Block; - item.Parent = null; - if (index < Count) - { - Array.Copy(_children, index + 1, _children, index, Count - index); - } - _children[Count] = default; + Array.Copy(_children, index + 1, _children, index, Count - index); } + _children[Count] = default; + } - public Block this[int index] + public Block this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get + var array = _children; + if ((uint)index >= (uint)array.Length || index >= Count) { - var array = _children; - if ((uint)index >= (uint)array.Length || index >= Count) - { - ThrowHelper.ThrowIndexOutOfRangeException(); - return null; - } - return array[index].Block; + ThrowHelper.ThrowIndexOutOfRangeException(); + return null; } - set - { - if ((uint)index >= (uint)Count) ThrowHelper.ThrowIndexOutOfRangeException(); + return array[index].Block; + } + set + { + if ((uint)index >= (uint)Count) ThrowHelper.ThrowIndexOutOfRangeException(); - if (value is null) - ThrowHelper.ArgumentNullException_item(); + if (value is null) + ThrowHelper.ArgumentNullException_item(); - if (value.Parent != null) - ThrowHelper.ArgumentException("Cannot add this block as it as already attached to another container (block.Parent != null)"); + if (value.Parent != null) + ThrowHelper.ArgumentException("Cannot add this block as it as already attached to another container (block.Parent != null)"); - var existingChild = _children[index].Block; - if (existingChild != null) - existingChild.Parent = null; + var existingChild = _children[index].Block; + if (existingChild != null) + existingChild.Parent = null; - value.Parent = this; - _children[index] = new BlockWrapper(value); - } + value.Parent = this; + _children[index] = new BlockWrapper(value); } + } - public void Sort(IComparer comparer) - { - if (comparer is null) ThrowHelper.ArgumentNullException(nameof(comparer)); - Array.Sort(_children, 0, Count, new BlockComparerWrapper(comparer)); - } + public void Sort(IComparer comparer) + { + if (comparer is null) ThrowHelper.ArgumentNullException(nameof(comparer)); + Array.Sort(_children, 0, Count, new BlockComparerWrapper(comparer)); + } + + public void Sort(Comparison comparison) + { + if (comparison is null) ThrowHelper.ArgumentNullException(nameof(comparison)); + Array.Sort(_children, 0, Count, new BlockComparisonWrapper(comparison)); + } + + #region Nested type: Enumerator + + [StructLayout(LayoutKind.Sequential)] + public struct Enumerator : IEnumerator + { + private readonly ContainerBlock block; + private int index; + private Block? current; - public void Sort(Comparison comparison) + internal Enumerator(ContainerBlock block) { - if (comparison is null) ThrowHelper.ArgumentNullException(nameof(comparison)); - Array.Sort(_children, 0, Count, new BlockComparisonWrapper(comparison)); + this.block = block; + index = 0; + current = null; } - #region Nested type: Enumerator + public Block Current => current!; + + object IEnumerator.Current => Current; - [StructLayout(LayoutKind.Sequential)] - public struct Enumerator : IEnumerator + public void Dispose() { - private readonly ContainerBlock block; - private int index; - private Block? current; + } - internal Enumerator(ContainerBlock block) + public bool MoveNext() + { + if (index < block.Count) { - this.block = block; - index = 0; - current = null; + current = block[index]; + index++; + return true; } + return MoveNextRare(); + } - public Block Current => current!; - - object IEnumerator.Current => Current; + private bool MoveNextRare() + { + index = block.Count + 1; + current = null; + return false; + } - public void Dispose() - { - } + void IEnumerator.Reset() + { + index = 0; + current = null; + } + } - public bool MoveNext() - { - if (index < block.Count) - { - current = block[index]; - index++; - return true; - } - return MoveNextRare(); - } + #endregion - private bool MoveNextRare() - { - index = block.Count + 1; - current = null; - return false; - } + private sealed class BlockComparisonWrapper : IComparer + { + private readonly Comparison _comparison; - void IEnumerator.Reset() - { - index = 0; - current = null; - } + public BlockComparisonWrapper(Comparison comparison) + { + _comparison = comparison; } - #endregion - - private sealed class BlockComparisonWrapper : IComparer + public int Compare(BlockWrapper x, BlockWrapper y) { - private readonly Comparison _comparison; + return _comparison(x.Block, y.Block); + } + } - public BlockComparisonWrapper(Comparison comparison) - { - _comparison = comparison; - } + private sealed class BlockComparerWrapper : IComparer + { + private readonly IComparer _comparer; - public int Compare(BlockWrapper x, BlockWrapper y) - { - return _comparison(x.Block, y.Block); - } + public BlockComparerWrapper(IComparer comparer) + { + _comparer = comparer; } - private sealed class BlockComparerWrapper : IComparer + public int Compare(BlockWrapper x, BlockWrapper y) { - private readonly IComparer _comparer; - - public BlockComparerWrapper(IComparer comparer) - { - _comparer = comparer; - } - - public int Compare(BlockWrapper x, BlockWrapper y) - { - return _comparer.Compare(x.Block, y.Block); - } + return _comparer.Compare(x.Block, y.Block); } } } \ No newline at end of file diff --git a/src/Markdig/Syntax/FencedCodeBlock.cs b/src/Markdig/Syntax/FencedCodeBlock.cs index b38b351ce..f2c08bb35 100644 --- a/src/Markdig/Syntax/FencedCodeBlock.cs +++ b/src/Markdig/Syntax/FencedCodeBlock.cs @@ -5,83 +5,82 @@ using Markdig.Helpers; using Markdig.Parsers; -namespace Markdig.Syntax +namespace Markdig.Syntax; + +/// +/// Represents a fenced code block. +/// +/// +/// Related to CommonMark spec: 4.5 Fenced code blocks +/// +public class FencedCodeBlock : CodeBlock, IFencedBlock { + private TriviaProperties? _trivia => TryGetDerivedTrivia(); + private TriviaProperties Trivia => GetOrSetDerivedTrivia(); + + /// + /// Initializes a new instance of the class. + /// + /// The parser. + public FencedCodeBlock(BlockParser parser) : base(parser) + { + // Fenced code blocks are not breakable, unless + // we reach: + // - a fenced line terminator + // - the closing of the container that is holding this fenced block + IsBreakable = false; + } + /// - /// Represents a fenced code block. + /// Gets or sets the indent count when the fenced code block was indented + /// and we need to remove up to indent count chars spaces from the beginning of a line. /// - /// - /// Related to CommonMark spec: 4.5 Fenced code blocks - /// - public class FencedCodeBlock : CodeBlock, IFencedBlock + public int IndentCount { get; set; } + + /// + public char FencedChar { get; set; } + + /// + public int OpeningFencedCharCount { get; set; } + + /// + public StringSlice TriviaAfterFencedChar { get => _trivia?.TriviaAfterFencedChar ?? StringSlice.Empty; set => Trivia.TriviaAfterFencedChar = value; } + + /// + public string? Info { get; set; } + + /// + public StringSlice UnescapedInfo { get => _trivia?.UnescapedInfo ?? StringSlice.Empty; set => Trivia.UnescapedInfo = value; } + + /// + public StringSlice TriviaAfterInfo { get => _trivia?.TriviaAfterInfo ?? StringSlice.Empty; set => Trivia.TriviaAfterInfo = value; } + + /// + public string? Arguments { get; set; } + + /// + public StringSlice UnescapedArguments { get => _trivia?.UnescapedArguments ?? StringSlice.Empty; set => Trivia.UnescapedArguments = value; } + + /// + public StringSlice TriviaAfterArguments { get => _trivia?.TriviaAfterArguments ?? StringSlice.Empty; set => Trivia.TriviaAfterArguments = value; } + + /// + public NewLine InfoNewLine { get => _trivia?.InfoNewLine ?? NewLine.None; set => Trivia.InfoNewLine = value; } + + /// + public StringSlice TriviaBeforeClosingFence { get => _trivia?.TriviaBeforeClosingFence ?? StringSlice.Empty; set => Trivia.TriviaBeforeClosingFence = value; } + + /// + public int ClosingFencedCharCount { get; set; } + + private sealed class TriviaProperties { - private TriviaProperties? _trivia => TryGetDerivedTrivia(); - private TriviaProperties Trivia => GetOrSetDerivedTrivia(); - - /// - /// Initializes a new instance of the class. - /// - /// The parser. - public FencedCodeBlock(BlockParser parser) : base(parser) - { - // Fenced code blocks are not breakable, unless - // we reach: - // - a fenced line terminator - // - the closing of the container that is holding this fenced block - IsBreakable = false; - } - - /// - /// Gets or sets the indent count when the fenced code block was indented - /// and we need to remove up to indent count chars spaces from the begining of a line. - /// - public int IndentCount { get; set; } - - /// - public char FencedChar { get; set; } - - /// - public int OpeningFencedCharCount { get; set; } - - /// - public StringSlice TriviaAfterFencedChar { get => _trivia?.TriviaAfterFencedChar ?? StringSlice.Empty; set => Trivia.TriviaAfterFencedChar = value; } - - /// - public string? Info { get; set; } - - /// - public StringSlice UnescapedInfo { get => _trivia?.UnescapedInfo ?? StringSlice.Empty; set => Trivia.UnescapedInfo = value; } - - /// - public StringSlice TriviaAfterInfo { get => _trivia?.TriviaAfterInfo ?? StringSlice.Empty; set => Trivia.TriviaAfterInfo = value; } - - /// - public string? Arguments { get; set; } - - /// - public StringSlice UnescapedArguments { get => _trivia?.UnescapedArguments ?? StringSlice.Empty; set => Trivia.UnescapedArguments = value; } - - /// - public StringSlice TriviaAfterArguments { get => _trivia?.TriviaAfterArguments ?? StringSlice.Empty; set => Trivia.TriviaAfterArguments = value; } - - /// - public NewLine InfoNewLine { get => _trivia?.InfoNewLine ?? NewLine.None; set => Trivia.InfoNewLine = value; } - - /// - public StringSlice TriviaBeforeClosingFence { get => _trivia?.TriviaBeforeClosingFence ?? StringSlice.Empty; set => Trivia.TriviaBeforeClosingFence = value; } - - /// - 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; - } + public StringSlice TriviaAfterFencedChar; + public StringSlice UnescapedInfo; + public StringSlice TriviaAfterInfo; + public StringSlice UnescapedArguments; + public StringSlice TriviaAfterArguments; + public NewLine InfoNewLine; + public StringSlice TriviaBeforeClosingFence; } } \ No newline at end of file diff --git a/src/Markdig/Syntax/HeadingBlock.cs b/src/Markdig/Syntax/HeadingBlock.cs index c6d18a5a0..6717779a3 100644 --- a/src/Markdig/Syntax/HeadingBlock.cs +++ b/src/Markdig/Syntax/HeadingBlock.cs @@ -6,63 +6,62 @@ using Markdig.Helpers; using Markdig.Parsers; -namespace Markdig.Syntax +namespace Markdig.Syntax; + +/// +/// Represents a heading. +/// +[DebuggerDisplay("{GetType().Name} Line: {Line}, {Lines} Level: {Level}")] +public class HeadingBlock : LeafBlock { + private TriviaProperties? _trivia => TryGetDerivedTrivia(); + private TriviaProperties Trivia => GetOrSetDerivedTrivia(); + /// - /// Represents a heading. + /// Initializes a new instance of the class. /// - [DebuggerDisplay("{GetType().Name} Line: {Line}, {Lines} Level: {Level}")] - public class HeadingBlock : LeafBlock + /// The parser. + public HeadingBlock(BlockParser parser) : base(parser) { - private TriviaProperties? _trivia => TryGetDerivedTrivia(); - private TriviaProperties Trivia => GetOrSetDerivedTrivia(); - - /// - /// Initializes a new instance of the class. - /// - /// The parser. - public HeadingBlock(BlockParser parser) : base(parser) - { - ProcessInlines = true; - } + ProcessInlines = true; + } - /// - /// Gets or sets the header character used to defines this heading (usually #) - /// - public char HeaderChar { get; set; } + /// + /// Gets or sets the header character used to defines this heading (usually #) + /// + public char HeaderChar { get; set; } - /// - /// Gets or sets the level of heading (starting at 1 for the lowest level). - /// - public int Level { get; set; } + /// + /// Gets or sets the level of heading (starting at 1 for the lowest level). + /// + public int Level { get; set; } - /// - /// True if this heading is a Setext heading. - /// - public bool IsSetext { get; set; } + /// + /// True if this heading is a Setext heading. + /// + public bool IsSetext { get; set; } - /// - /// Gets or sets the amount of - or = characters when is true. - /// - public int HeaderCharCount { get; set; } + /// + /// Gets or sets the amount of - or = characters when is true. + /// + public int HeaderCharCount { get; set; } - /// - /// Gets or sets the newline of the first line when is true. - /// Trivia: only parsed when is enabled. - /// - public NewLine SetextNewline { get => _trivia?.SetextNewline ?? NewLine.None; set => Trivia.SetextNewline = value; } + /// + /// Gets or sets the newline of the first line when is true. + /// Trivia: only parsed when is enabled. + /// + public NewLine SetextNewline { get => _trivia?.SetextNewline ?? NewLine.None; set => Trivia.SetextNewline = value; } - /// - /// Gets or sets the whitespace after the # character when is false. - /// Trivia: only parsed when is enabled, otherwise - /// . - /// - public StringSlice TriviaAfterAtxHeaderChar { get => _trivia?.TriviaAfterAtxHeaderChar ?? StringSlice.Empty; set => Trivia.TriviaAfterAtxHeaderChar = value; } + /// + /// Gets or sets the whitespace after the # character when is false. + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + public StringSlice TriviaAfterAtxHeaderChar { get => _trivia?.TriviaAfterAtxHeaderChar ?? StringSlice.Empty; set => Trivia.TriviaAfterAtxHeaderChar = value; } - private sealed class TriviaProperties - { - public NewLine SetextNewline; - public StringSlice TriviaAfterAtxHeaderChar; - } + private sealed class TriviaProperties + { + public NewLine SetextNewline; + public StringSlice TriviaAfterAtxHeaderChar; } } \ No newline at end of file diff --git a/src/Markdig/Syntax/HtmlBlock.cs b/src/Markdig/Syntax/HtmlBlock.cs index 1cafd31c0..c8358622b 100644 --- a/src/Markdig/Syntax/HtmlBlock.cs +++ b/src/Markdig/Syntax/HtmlBlock.cs @@ -4,25 +4,24 @@ using Markdig.Parsers; -namespace Markdig.Syntax +namespace Markdig.Syntax; + +/// +/// Represents a group of lines that is treated as raw HTML (and will not be escaped in HTML output). +/// +/// +public class HtmlBlock : LeafBlock { /// - /// Represents a group of lines that is treated as raw HTML (and will not be escaped in HTML output). + /// Initializes a new instance of the class. /// - /// - public class HtmlBlock : LeafBlock + /// The parser. + public HtmlBlock(BlockParser? parser) : base(parser) { - /// - /// Initializes a new instance of the class. - /// - /// The parser. - public HtmlBlock(BlockParser? parser) : base(parser) - { - } - - /// - /// Gets or sets the type of block. - /// - public HtmlBlockType Type { get; set; } } + + /// + /// Gets or sets the type of block. + /// + public HtmlBlockType Type { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Syntax/HtmlBlockType.cs b/src/Markdig/Syntax/HtmlBlockType.cs index c4b3bc126..148242263 100644 --- a/src/Markdig/Syntax/HtmlBlockType.cs +++ b/src/Markdig/Syntax/HtmlBlockType.cs @@ -2,46 +2,45 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -namespace Markdig.Syntax +namespace Markdig.Syntax; + +/// +/// Defines the type of +/// +public enum HtmlBlockType { /// - /// Defines the type of + /// A SGML document type starting by <!LETTER. + /// + DocumentType, + + /// + /// A raw CDATA sequence. + /// + CData, + + /// + /// A HTML comment. + /// + Comment, + + /// + /// A SGM processing instruction tag <? + /// + ProcessingInstruction, + + /// + /// A script pre or style tag. + /// + ScriptPreOrStyle, + + /// + /// An HTML interrupting block + /// + InterruptingBlock, + + /// + /// An HTML non-interrupting block /// - public enum HtmlBlockType - { - /// - /// A SGML document type starting by <!LETTER. - /// - DocumentType, - - /// - /// A raw CDATA sequence. - /// - CData, - - /// - /// A HTML comment. - /// - Comment, - - /// - /// A SGM processing instruction tag <? - /// - ProcessingInstruction, - - /// - /// A script pre or style tag. - /// - ScriptPreOrStyle, - - /// - /// An HTML interrupting block - /// - InterruptingBlock, - - /// - /// An HTML non-interrupting block - /// - NonInterruptingBlock - } + NonInterruptingBlock } \ No newline at end of file diff --git a/src/Markdig/Syntax/IBlock.cs b/src/Markdig/Syntax/IBlock.cs index 441157c96..6efa3fad1 100644 --- a/src/Markdig/Syntax/IBlock.cs +++ b/src/Markdig/Syntax/IBlock.cs @@ -5,69 +5,68 @@ using Markdig.Helpers; using Markdig.Parsers; -namespace Markdig.Syntax +namespace Markdig.Syntax; + +/// +/// Base interface for a block structure. Either a or a . +/// +/// +public interface IBlock : IMarkdownObject { /// - /// Base interface for a block structure. Either a or a . + /// Gets or sets the text column this instance was declared (zero-based). /// - /// - public interface IBlock : IMarkdownObject - { - /// - /// Gets or sets the text column this instance was declared (zero-based). - /// - int Column { get; set; } + int Column { get; set; } - /// - /// Gets or sets the text line this instance was declared (zero-based). - /// - int Line { get; set; } + /// + /// Gets or sets the text line this instance was declared (zero-based). + /// + int Line { get; set; } - /// - /// Gets the parent of this container. May be null. - /// - ContainerBlock? Parent { get; } + /// + /// Gets the parent of this container. May be null. + /// + ContainerBlock? Parent { get; } - /// - /// Gets the parser associated to this instance. - /// - BlockParser? Parser { get; } + /// + /// Gets the parser associated to this instance. + /// + BlockParser? Parser { get; } - /// - /// Gets or sets a value indicating whether this instance is still open. - /// - bool IsOpen { get; set; } + /// + /// Gets or sets a value indicating whether this instance is still open. + /// + bool IsOpen { get; set; } - /// - /// Gets or sets a value indicating whether this block is breakable. Default is true. - /// - bool IsBreakable { get; set; } + /// + /// Gets or sets a value indicating whether this block is breakable. Default is true. + /// + bool IsBreakable { get; set; } - /// - /// Gets or sets a value indicating whether this block must be removed from its container after inlines have been processed. - /// - bool RemoveAfterProcessInlines { get; set; } + /// + /// Gets or sets a value indicating whether this block must be removed from its container after inlines have been processed. + /// + bool RemoveAfterProcessInlines { get; set; } - /// - /// Occurs when the process of inlines begin. - /// - event ProcessInlineDelegate? ProcessInlinesBegin; + /// + /// Occurs when the process of inlines begin. + /// + event ProcessInlineDelegate? ProcessInlinesBegin; - /// - /// Occurs when the process of inlines ends for this instance. - /// - event ProcessInlineDelegate? ProcessInlinesEnd; + /// + /// Occurs when the process of inlines ends for this instance. + /// + event ProcessInlineDelegate? ProcessInlinesEnd; - /// - /// Trivia occurring before this block - /// - /// Trivia: only parsed when is enabled, otherwise . - StringSlice TriviaBefore { get; set; } + /// + /// Trivia occurring before this block + /// + /// Trivia: only parsed when is enabled, otherwise . + StringSlice TriviaBefore { get; set; } - /// - /// Trivia occurring after this block - /// - /// Trivia: only parsed when is enabled, otherwise . - StringSlice TriviaAfter { get; set; } - } + /// + /// Trivia occurring after this block + /// + /// Trivia: only parsed when is enabled, otherwise . + StringSlice TriviaAfter { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Syntax/IFencedBlock.cs b/src/Markdig/Syntax/IFencedBlock.cs index 7b0720423..f91e378a5 100644 --- a/src/Markdig/Syntax/IFencedBlock.cs +++ b/src/Markdig/Syntax/IFencedBlock.cs @@ -4,95 +4,94 @@ using Markdig.Helpers; -namespace Markdig.Syntax +namespace Markdig.Syntax; + +/// +/// A common interface for fenced block (e.g: or ) +/// +public interface IFencedBlock : IBlock { /// - /// A common interface for fenced block (e.g: or ) + /// Gets or sets the fenced character used to open and close this fenced code block. /// - public interface IFencedBlock : IBlock - { - /// - /// Gets or sets the fenced character used to open and close this fenced code block. - /// - char FencedChar { get; set; } + char FencedChar { get; set; } - /// - /// Gets or sets the fenced character count used to open this fenced code block. - /// - int OpeningFencedCharCount { get; set; } + /// + /// Gets or sets the fenced character count used to open this fenced code block. + /// + int OpeningFencedCharCount { get; set; } - /// - /// Gets or sets the trivia after the . - /// Trivia: only parsed when is enabled, otherwise - /// . - /// - StringSlice TriviaAfterFencedChar { get; set; } + /// + /// Gets or sets the trivia after the . + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + StringSlice TriviaAfterFencedChar { get; set; } - /// - /// Gets or sets the language parsed after the first line of - /// the fenced code block. May be null. - /// - string? Info { get; set; } + /// + /// Gets or sets the language parsed after the first line of + /// the fenced code block. May be null. + /// + string? Info { get; set; } - /// - /// Non-escaped exactly as in source markdown. - /// Trivia: only parsed when is enabled, otherwise - /// . - /// - StringSlice UnescapedInfo { get; set; } + /// + /// Non-escaped exactly as in source markdown. + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + StringSlice UnescapedInfo { get; set; } - /// - /// Gets or sets the trivia after the . - /// Trivia: only parsed when is enabled, otherwise - /// . - /// - StringSlice TriviaAfterInfo { get; set; } + /// + /// Gets or sets the trivia after the . + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + StringSlice TriviaAfterInfo { get; set; } - /// - /// Gets or sets the arguments after the . - /// May be null. - /// - string? Arguments { get; set; } + /// + /// Gets or sets the arguments after the . + /// May be null. + /// + string? Arguments { get; set; } - /// - /// Non-escaped exactly as in source markdown. - /// Trivia: only parsed when is enabled, otherwise - /// . - /// - StringSlice UnescapedArguments { get; set; } + /// + /// Non-escaped exactly as in source markdown. + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + StringSlice UnescapedArguments { get; set; } - /// - /// Gets or sets the trivia after the . - /// Trivia: only parsed when is enabled, otherwise - /// . - /// - StringSlice TriviaAfterArguments { get; set; } + /// + /// Gets or sets the trivia after the . + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + StringSlice TriviaAfterArguments { get; set; } - /// - /// Newline of the line with the opening fenced chars. - /// Trivia: only parsed when is enabled, otherwise - /// . - /// - NewLine InfoNewLine { get; set; } + /// + /// Newline of the line with the opening fenced chars. + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + NewLine InfoNewLine { get; set; } - /// - /// Trivia before the closing fenced chars - /// Trivia: only parsed when is enabled, otherwise - /// . - /// - StringSlice TriviaBeforeClosingFence { get; set; } + /// + /// Trivia before the closing fenced chars + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + StringSlice TriviaBeforeClosingFence { get; set; } - /// - /// Gets or sets the fenced character count used to close this fenced code block. - /// - int ClosingFencedCharCount { get; set; } + /// + /// Gets or sets the fenced character count used to close this fenced code block. + /// + int ClosingFencedCharCount { get; set; } - /// - /// Newline after the last line, which is always the line containing the closing fence chars. - /// "Inherited" from . - /// Trivia: only parsed when is enabled, otherwise - /// . - /// - NewLine NewLine { get; set; } - } + /// + /// Newline after the last line, which is always the line containing the closing fence chars. + /// "Inherited" from . + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + NewLine NewLine { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Syntax/IMarkdownObject.cs b/src/Markdig/Syntax/IMarkdownObject.cs index 103832c52..3fd77f149 100644 --- a/src/Markdig/Syntax/IMarkdownObject.cs +++ b/src/Markdig/Syntax/IMarkdownObject.cs @@ -2,43 +2,42 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -namespace Markdig.Syntax +namespace Markdig.Syntax; + +/// +/// Base interface for a the Markdown syntax tree +/// +public interface IMarkdownObject { /// - /// Base interface for a the Markdown syntax tree + /// Stores a key/value pair for this instance. /// - public interface IMarkdownObject - { - /// - /// Stores a key/value pair for this instance. - /// - /// The key. - /// The value. - /// if key is null - void SetData(object key, object value); + /// The key. + /// The value. + /// if key is null + void SetData(object key, object value); - /// - /// Determines whether this instance contains the specified key data. - /// - /// The key. - /// true if a data with the key is stored - /// if key is null - bool ContainsData(object key); + /// + /// Determines whether this instance contains the specified key data. + /// + /// The key. + /// true if a data with the key is stored + /// if key is null + bool ContainsData(object key); - /// - /// Gets the associated data for the specified key. - /// - /// The key. - /// The associated data or null if none - /// if key is null - object? GetData(object key); + /// + /// Gets the associated data for the specified key. + /// + /// The key. + /// The associated data or null if none + /// if key is null + object? GetData(object key); - /// - /// Removes the associated data for the specified key. - /// - /// The key. - /// true if the data was removed; false otherwise - /// - bool RemoveData(object key); - } + /// + /// Removes the associated data for the specified key. + /// + /// The key. + /// true if the data was removed; false otherwise + /// + bool RemoveData(object key); } \ No newline at end of file diff --git a/src/Markdig/Syntax/Inlines/AutolinkInline.cs b/src/Markdig/Syntax/Inlines/AutolinkInline.cs index eb25a46a0..d82f24749 100644 --- a/src/Markdig/Syntax/Inlines/AutolinkInline.cs +++ b/src/Markdig/Syntax/Inlines/AutolinkInline.cs @@ -4,33 +4,32 @@ using System.Diagnostics; -namespace Markdig.Syntax.Inlines +namespace Markdig.Syntax.Inlines; + +/// +/// An autolink (Section 6.7 CommonMark specs) +/// +/// +[DebuggerDisplay("<{Url}>")] +public sealed class AutolinkInline : LeafInline { - /// - /// An autolink (Section 6.7 CommonMark specs) - /// - /// - [DebuggerDisplay("<{Url}>")] - public sealed class AutolinkInline : LeafInline + public AutolinkInline(string url) { - public AutolinkInline(string url) - { - Url = url; - } + Url = url; + } - /// - /// Gets or sets a value indicating whether this instance is an email link. - /// - public bool IsEmail { get; set; } + /// + /// Gets or sets a value indicating whether this instance is an email link. + /// + public bool IsEmail { get; set; } - /// - /// Gets or sets the URL of this link. - /// - public string Url { get; set; } + /// + /// Gets or sets the URL of this link. + /// + public string Url { get; set; } - public override string ToString() - { - return Url; - } + public override string ToString() + { + return Url; } } \ No newline at end of file diff --git a/src/Markdig/Syntax/Inlines/CodeInline.cs b/src/Markdig/Syntax/Inlines/CodeInline.cs index 00a524d2a..592266f1f 100644 --- a/src/Markdig/Syntax/Inlines/CodeInline.cs +++ b/src/Markdig/Syntax/Inlines/CodeInline.cs @@ -2,62 +2,61 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using Markdig.Helpers; -using System; using System.Diagnostics; -namespace Markdig.Syntax.Inlines +using Markdig.Helpers; + +namespace Markdig.Syntax.Inlines; + +/// +/// Represents a code span (Section 6.3 CommonMark specs) +/// +/// +[DebuggerDisplay("`{Content}`")] +public class CodeInline : LeafInline { + private TriviaProperties? _trivia => GetTrivia(); + private TriviaProperties Trivia => GetOrSetTrivia(); + + private LazySubstring _content; + + public CodeInline(string content) : this(new LazySubstring(content)) { } + + internal CodeInline(LazySubstring content) + { + _content = content; + } + /// - /// Represents a code span (Section 6.3 CommonMark specs) + /// Gets or sets the delimiter character used by this code inline. /// - /// - [DebuggerDisplay("`{Content}`")] - public class CodeInline : LeafInline + public char Delimiter { get; set; } + + /// + /// Gets or sets the amount of delimiter characters used + /// + public int DelimiterCount { get; set; } + + /// + /// Gets or sets the content of the span. + /// + public string Content + { + get => _content.ToString(); + set => _content = new LazySubstring(value ?? string.Empty); + } + + public ReadOnlySpan ContentSpan => _content.AsSpan(); + + /// + /// Gets or sets the content with trivia and whitespace. + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + public StringSlice ContentWithTrivia { get => _trivia?.ContentWithTrivia ?? StringSlice.Empty; set => Trivia.ContentWithTrivia = value; } + + private sealed class TriviaProperties { - private TriviaProperties? _trivia => GetTrivia(); - private TriviaProperties Trivia => GetOrSetTrivia(); - - private LazySubstring _content; - - public CodeInline(string content) : this(new LazySubstring(content)) { } - - internal CodeInline(LazySubstring content) - { - _content = content; - } - - /// - /// Gets or sets the delimiter character used by this code inline. - /// - public char Delimiter { get; set; } - - /// - /// Gets or sets the amount of delimiter characters used - /// - public int DelimiterCount { get; set; } - - /// - /// Gets or sets the content of the span. - /// - public string Content - { - get => _content.ToString(); - set => _content = new LazySubstring(value ?? string.Empty); - } - - public ReadOnlySpan ContentSpan => _content.AsSpan(); - - /// - /// Gets or sets the content with trivia and whitespace. - /// Trivia: only parsed when is enabled, otherwise - /// . - /// - public StringSlice ContentWithTrivia { get => _trivia?.ContentWithTrivia ?? StringSlice.Empty; set => Trivia.ContentWithTrivia = value; } - - private sealed class TriviaProperties - { - public StringSlice ContentWithTrivia; - } + public StringSlice ContentWithTrivia; } } \ No newline at end of file diff --git a/src/Markdig/Syntax/Inlines/ContainerInline.cs b/src/Markdig/Syntax/Inlines/ContainerInline.cs index 404d21d14..3aaa1f555 100644 --- a/src/Markdig/Syntax/Inlines/ContainerInline.cs +++ b/src/Markdig/Syntax/Inlines/ContainerInline.cs @@ -2,297 +2,295 @@ // 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.Collections; -using System.Collections.Generic; using System.Diagnostics; using System.IO; + using Markdig.Helpers; -namespace Markdig.Syntax.Inlines +namespace Markdig.Syntax.Inlines; + +/// +/// A base class for container for . +/// +/// +public class ContainerInline : Inline, IEnumerable { + public ContainerInline() + { + SetTypeKind(isInline: true, isContainer: true); + } + + /// + /// Gets the parent block of this inline. + /// + public LeafBlock? ParentBlock { get; internal set; } + /// - /// A base class for container for . + /// Gets the first child. /// - /// - public class ContainerInline : Inline, IEnumerable + public Inline? FirstChild { get; private set; } + + /// + /// Gets the last child. + /// + public Inline? LastChild { get; private set; } + + /// + /// Clears this instance by removing all its children. + /// + public void Clear() { - public ContainerInline() + var child = LastChild; + while (child != null) { - SetTypeKind(isInline: true, isContainer: true); + child.Parent = null; + child = child.PreviousSibling; } + FirstChild = null; + LastChild = null; + } - /// - /// Gets the parent block of this inline. - /// - public LeafBlock? ParentBlock { get; internal set; } - - /// - /// Gets the first child. - /// - public Inline? FirstChild { get; private set; } - - /// - /// Gets the last child. - /// - public Inline? LastChild { get; private set; } - - /// - /// Clears this instance by removing all its children. - /// - public void Clear() + /// + /// Appends a child to this container. + /// + /// The child to append to this container.. + /// This instance + /// If child is null + /// Inline has already a parent + public virtual ContainerInline AppendChild(Inline child) + { + if (child is null) ThrowHelper.ArgumentNullException(nameof(child)); + if (child.Parent != null) { - var child = LastChild; - while (child != null) - { - child.Parent = null; - child = child.PreviousSibling; - } - FirstChild = null; - LastChild = null; + ThrowHelper.ArgumentException("Inline has already a parent", nameof(child)); } - /// - /// Appends a child to this container. - /// - /// The child to append to this container.. - /// This instance - /// If child is null - /// Inline has already a parent - public virtual ContainerInline AppendChild(Inline child) + if (FirstChild is null) { - if (child is null) ThrowHelper.ArgumentNullException(nameof(child)); - if (child.Parent != null) - { - ThrowHelper.ArgumentException("Inline has already a parent", nameof(child)); - } - - if (FirstChild is null) - { - FirstChild = child; - LastChild = child; - child.Parent = this; - } - else - { - LastChild!.InsertAfter(child); - } - return this; + FirstChild = child; + LastChild = child; + child.Parent = this; } + else + { + LastChild!.InsertAfter(child); + } + return this; + } - /// - /// Checks if this instance contains the specified child. - /// - /// The child to find. - /// true if this instance contains the specified child; false otherwise - public bool ContainsChild(Inline childToFind) + /// + /// Checks if this instance contains the specified child. + /// + /// The child to find. + /// true if this instance contains the specified child; false otherwise + public bool ContainsChild(Inline childToFind) + { + var child = FirstChild; + while (child != null) { - var child = FirstChild; - while (child != null) + var next = child.NextSibling; + if (child == childToFind) { - var next = child.NextSibling; - if (child == childToFind) - { - return true; - } - child = next; + return true; } - return false; + child = next; } + return false; + } - /// - /// Finds all the descendants. - /// - /// Type of the descendants to find - /// An enumeration of T - public IEnumerable FindDescendants() where T : Inline + /// + /// Finds all the descendants. + /// + /// Type of the descendants to find + /// An enumeration of T + public IEnumerable FindDescendants() where T : Inline + { + if (FirstChild is null) { - if (FirstChild is null) - { - return Array.Empty(); - } - else - { - return FindDescendantsInternal(); - } + return Array.Empty(); + } + else + { + return FindDescendantsInternal(); } - internal IEnumerable FindDescendantsInternal() where T : MarkdownObject + } + internal IEnumerable FindDescendantsInternal() where T : MarkdownObject + { + Debug.Assert(typeof(T).IsSubclassOf(typeof(Inline))); + + var stack = new Stack(); + + var child = LastChild; + while (child != null) { - Debug.Assert(typeof(T).IsSubclassOf(typeof(Inline))); + stack.Push(child); + child = child.PreviousSibling; + } - var stack = new Stack(); + while (stack.Count > 0) + { + child = stack.Pop(); - var child = LastChild; - while (child != null) + if (child is T childT) { - stack.Push(child); - child = child.PreviousSibling; + yield return childT; } - while (stack.Count > 0) + if (child is ContainerInline containerInline) { - child = stack.Pop(); - - if (child is T childT) - { - yield return childT; - } - - if (child is ContainerInline containerInline) + child = containerInline.LastChild; + while (child != null) { - child = containerInline.LastChild; - while (child != null) - { - stack.Push(child); - child = child.PreviousSibling; - } + stack.Push(child); + child = child.PreviousSibling; } } } + } - /// - /// Moves all the children of this container after the specified inline. - /// - /// The parent. - public void MoveChildrenAfter(Inline parent) + /// + /// Moves all the children of this container after the specified inline. + /// + /// The parent. + public void MoveChildrenAfter(Inline parent) + { + if (parent is null) ThrowHelper.ArgumentNullException(nameof(parent)); + var child = FirstChild; + var nextSibling = parent; + while (child != null) { - if (parent is null) ThrowHelper.ArgumentNullException(nameof(parent)); - var child = FirstChild; - var nextSibling = parent; - while (child != null) - { - var next = child.NextSibling; - // TODO: optimize this - child.Remove(); - nextSibling.InsertAfter(child); - nextSibling = child; - child = next; - } + var next = child.NextSibling; + // TODO: optimize this + child.Remove(); + nextSibling.InsertAfter(child); + nextSibling = child; + child = next; } + } - /// - /// Embraces this instance by the specified container. - /// - /// The container to use to embrace this instance. - /// If the container is null - public void EmbraceChildrenBy(ContainerInline container) + /// + /// Embraces this instance by the specified container. + /// + /// The container to use to embrace this instance. + /// If the container is null + public void EmbraceChildrenBy(ContainerInline container) + { + if (container is null) ThrowHelper.ArgumentNullException(nameof(container)); + var child = FirstChild; + while (child != null) { - if (container is null) ThrowHelper.ArgumentNullException(nameof(container)); - var child = FirstChild; - while (child != null) - { - var next = child.NextSibling; - // TODO: optimize this - child.Remove(); - container.AppendChild(child); - child = next; - } - AppendChild(container); + var next = child.NextSibling; + // TODO: optimize this + child.Remove(); + container.AppendChild(child); + child = next; } + AppendChild(container); + } - protected override void OnChildInsert(Inline child) + protected override void OnChildInsert(Inline child) + { + // A child is inserted before the FirstChild + if (child.PreviousSibling is null && child.NextSibling == FirstChild) { - // A child is inserted before the FirstChild - if (child.PreviousSibling is null && child.NextSibling == FirstChild) - { - FirstChild = child; - } - else if (child.NextSibling is null && child.PreviousSibling == LastChild) - { - LastChild = child; - } + FirstChild = child; + } + else if (child.NextSibling is null && child.PreviousSibling == LastChild) + { + LastChild = child; + } - if (LastChild is null) - { - LastChild = FirstChild; - } - else if (FirstChild is null) - { - FirstChild = LastChild; - } + if (LastChild is null) + { + LastChild = FirstChild; + } + else if (FirstChild is null) + { + FirstChild = LastChild; } + } - protected override void OnChildRemove(Inline child) + protected override void OnChildRemove(Inline child) + { + if (child == FirstChild) { - if (child == FirstChild) + if (FirstChild == LastChild) { - if (FirstChild == LastChild) - { - FirstChild = null; - LastChild = null; - } - else - { - FirstChild = child.NextSibling ?? LastChild; - } + FirstChild = null; + LastChild = null; } - else if (child == LastChild) + else { - LastChild = child.PreviousSibling ?? FirstChild; + FirstChild = child.NextSibling ?? LastChild; } } - - protected override void DumpChildTo(TextWriter writer, int level) + else if (child == LastChild) { - if (FirstChild != null) - { - level++; - FirstChild.DumpTo(writer, level); - } + LastChild = child.PreviousSibling ?? FirstChild; } + } - public struct Enumerator : IEnumerator + protected override void DumpChildTo(TextWriter writer, int level) + { + if (FirstChild != null) { - private readonly ContainerInline container; - private Inline? currentChild; - private Inline? nextChild; + level++; + FirstChild.DumpTo(writer, level); + } + } - public Enumerator(ContainerInline container) : this() - { - if (container is null) ThrowHelper.ArgumentNullException(nameof(container)); - this.container = container; - currentChild = nextChild = container.FirstChild; - } + public struct Enumerator : IEnumerator + { + private readonly ContainerInline container; + private Inline? currentChild; + private Inline? nextChild; - public Inline Current => currentChild!; + public Enumerator(ContainerInline container) : this() + { + if (container is null) ThrowHelper.ArgumentNullException(nameof(container)); + this.container = container; + currentChild = nextChild = container.FirstChild; + } - object IEnumerator.Current => Current; + public Inline Current => currentChild!; - public void Dispose() - { - } + object IEnumerator.Current => Current; - public bool MoveNext() - { - currentChild = nextChild; - if (currentChild != null) - { - nextChild = currentChild.NextSibling; - return true; - } - nextChild = null; - return false; - } + public void Dispose() + { + } - public void Reset() + public bool MoveNext() + { + currentChild = nextChild; + if (currentChild != null) { - currentChild = nextChild = container.FirstChild; + nextChild = currentChild.NextSibling; + return true; } + nextChild = null; + return false; } - public Enumerator GetEnumerator() + public void Reset() { - return new Enumerator(this); + currentChild = nextChild = container.FirstChild; } + } - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); } } \ No newline at end of file diff --git a/src/Markdig/Syntax/Inlines/DelimiterInline.cs b/src/Markdig/Syntax/Inlines/DelimiterInline.cs index 1f144abfa..0b06c80df 100644 --- a/src/Markdig/Syntax/Inlines/DelimiterInline.cs +++ b/src/Markdig/Syntax/Inlines/DelimiterInline.cs @@ -6,54 +6,53 @@ using Markdig.Helpers; using Markdig.Parsers; -namespace Markdig.Syntax.Inlines +namespace Markdig.Syntax.Inlines; + +/// +/// Internal delimiter used by some parsers (e.g emphasis, tables). +/// +/// +[DebuggerDisplay("{ToLiteral()} {Type}")] +public abstract class DelimiterInline : ContainerInline { + protected DelimiterInline(InlineParser parser) + { + if (parser is null) ThrowHelper.ArgumentNullException(nameof(parser)); + Parser = parser; + IsActive = true; + } + /// - /// Internal delimiter used by some parsers (e.g emphasis, tables). + /// Gets the parser. /// - /// - [DebuggerDisplay("{ToLiteral()} {Type}")] - public abstract class DelimiterInline : ContainerInline + public InlineParser Parser { get; } + + /// + /// Gets or sets the type of this delimiter. + /// + public DelimiterType Type { get; set; } + + /// + /// Gets or sets a value indicating whether this instance is active. + /// + public bool IsActive { get; set; } + + /// + /// Converts this delimiter to a literal. + /// + /// The string representation of this delimiter + public abstract string ToLiteral(); + + public void ReplaceByLiteral() { - protected DelimiterInline(InlineParser parser) - { - if (parser is null) ThrowHelper.ArgumentNullException(nameof(parser)); - Parser = parser; - IsActive = true; - } - - /// - /// Gets the parser. - /// - public InlineParser Parser { get; } - - /// - /// Gets or sets the type of this delimiter. - /// - public DelimiterType Type { get; set; } - - /// - /// Gets or sets a value indicating whether this instance is active. - /// - public bool IsActive { get; set; } - - /// - /// Converts this delimiter to a literal. - /// - /// The string representation of this delimiter - public abstract string ToLiteral(); - - public void ReplaceByLiteral() + var literalInline = new LiteralInline() { - var literalInline = new LiteralInline() - { - Content = new StringSlice(ToLiteral()), - Span = Span, - Line = Line, - Column = Column, - IsClosed = true - }; - ReplaceBy(literalInline); - } + Content = new StringSlice(ToLiteral()), + Span = Span, + Line = Line, + Column = Column, + IsClosed = true + }; + ReplaceBy(literalInline); } } \ No newline at end of file diff --git a/src/Markdig/Syntax/Inlines/DelimiterType.cs b/src/Markdig/Syntax/Inlines/DelimiterType.cs index 865083fd9..ed1621166 100644 --- a/src/Markdig/Syntax/Inlines/DelimiterType.cs +++ b/src/Markdig/Syntax/Inlines/DelimiterType.cs @@ -2,29 +2,26 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using System; +namespace Markdig.Syntax.Inlines; -namespace Markdig.Syntax.Inlines +/// +/// Gets the type of a . +/// +[Flags] +public enum DelimiterType : byte { /// - /// Gets the type of a . + /// An undefined open or close delimiter. /// - [Flags] - public enum DelimiterType : byte - { - /// - /// An undefined open or close delimiter. - /// - Undefined, + Undefined, - /// - /// An open delimiter. - /// - Open, + /// + /// An open delimiter. + /// + Open, - /// - /// A close delimiter. - /// - Close, - } + /// + /// A close delimiter. + /// + Close, } \ No newline at end of file diff --git a/src/Markdig/Syntax/Inlines/EmphasisDelimiterInline.cs b/src/Markdig/Syntax/Inlines/EmphasisDelimiterInline.cs index 66419a313..aa70896e3 100644 --- a/src/Markdig/Syntax/Inlines/EmphasisDelimiterInline.cs +++ b/src/Markdig/Syntax/Inlines/EmphasisDelimiterInline.cs @@ -2,64 +2,62 @@ // 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 Markdig.Helpers; using Markdig.Parsers; using Markdig.Parsers.Inlines; -namespace Markdig.Syntax.Inlines +namespace Markdig.Syntax.Inlines; + +/// +/// A delimiter used for parsing emphasis. +/// +/// +public class EmphasisDelimiterInline : DelimiterInline { /// - /// A delimiter used for parsing emphasis. + /// Initializes a new instance of the class. /// - /// - public class EmphasisDelimiterInline : DelimiterInline + /// The parser. + /// The descriptor. + /// + public EmphasisDelimiterInline(InlineParser parser, EmphasisDescriptor descriptor) : base(parser) { - /// - /// Initializes a new instance of the class. - /// - /// The parser. - /// The descriptor. - /// - public EmphasisDelimiterInline(InlineParser parser, EmphasisDescriptor descriptor) : base(parser) - { - if (descriptor is null) - ThrowHelper.ArgumentNullException(nameof(descriptor)); + if (descriptor is null) + ThrowHelper.ArgumentNullException(nameof(descriptor)); - Descriptor = descriptor; - DelimiterChar = descriptor.Character; - } + Descriptor = descriptor; + DelimiterChar = descriptor.Character; + } - /// - /// Gets the descriptor for this emphasis. - /// - public EmphasisDescriptor Descriptor { get; } + /// + /// Gets the descriptor for this emphasis. + /// + public EmphasisDescriptor Descriptor { get; } - /// - /// The delimiter character found. - /// - public char DelimiterChar { get; } + /// + /// The delimiter character found. + /// + public char DelimiterChar { get; } - /// - /// The number of delimiter characters found for this delimiter. - /// - public int DelimiterCount { get; set; } + /// + /// The number of delimiter characters found for this delimiter. + /// + public int DelimiterCount { get; set; } - public override string ToLiteral() - { - return DelimiterCount > 0 ? new string(DelimiterChar, DelimiterCount) : string.Empty; - } + public override string ToLiteral() + { + return DelimiterCount > 0 ? new string(DelimiterChar, DelimiterCount) : string.Empty; + } - public LiteralInline AsLiteralInline() + public LiteralInline AsLiteralInline() + { + return new LiteralInline() { - return new LiteralInline() - { - Content = new StringSlice(ToLiteral()), - IsClosed = true, - Span = Span, - Line = Line, - Column = Column - }; - } + Content = new StringSlice(ToLiteral()), + IsClosed = true, + Span = Span, + Line = Line, + Column = Column + }; } } \ No newline at end of file diff --git a/src/Markdig/Syntax/Inlines/EmphasisInline.cs b/src/Markdig/Syntax/Inlines/EmphasisInline.cs index 1fa6c1f30..303e48bac 100644 --- a/src/Markdig/Syntax/Inlines/EmphasisInline.cs +++ b/src/Markdig/Syntax/Inlines/EmphasisInline.cs @@ -2,37 +2,35 @@ // 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; -namespace Markdig.Syntax.Inlines +namespace Markdig.Syntax.Inlines; + +/// +/// An emphasis and strong emphasis (Section 6.4 CommonMark specs). +/// +/// +[DebuggerDisplay("{DelimiterChar} Count: {DelimiterCount}")] +public class EmphasisInline : ContainerInline { /// - /// An emphasis and strong emphasis (Section 6.4 CommonMark specs). + /// Gets or sets the delimiter character of this emphasis. /// - /// - [DebuggerDisplay("{DelimiterChar} Count: {DelimiterCount}")] - public class EmphasisInline : ContainerInline - { - /// - /// Gets or sets the delimiter character of this emphasis. - /// - public char DelimiterChar { get; set; } - - /// - /// Gets or sets a value indicating whether this is strong. - /// Marked obsolete as EmphasisInline can now be represented by more than two delimiter characters - /// - [Obsolete("Use `DelimiterCount == 2` instead", error: false)] - public bool IsDouble - { - get => DelimiterCount == 2; - set => DelimiterCount = value ? 2 : 1; - } + public char DelimiterChar { get; set; } - /// - /// Gets or sets the number of delimiter characters for this emphasis. - /// - public int DelimiterCount { get; set; } + /// + /// Gets or sets a value indicating whether this is strong. + /// Marked obsolete as EmphasisInline can now be represented by more than two delimiter characters + /// + [Obsolete("Use `DelimiterCount == 2` instead", error: false)] + public bool IsDouble + { + get => DelimiterCount == 2; + set => DelimiterCount = value ? 2 : 1; } + + /// + /// Gets or sets the number of delimiter characters for this emphasis. + /// + public int DelimiterCount { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Syntax/Inlines/HtmlEntityInline.cs b/src/Markdig/Syntax/Inlines/HtmlEntityInline.cs index 17f93e042..ddeb37222 100644 --- a/src/Markdig/Syntax/Inlines/HtmlEntityInline.cs +++ b/src/Markdig/Syntax/Inlines/HtmlEntityInline.cs @@ -5,23 +5,22 @@ using System.Diagnostics; using Markdig.Helpers; -namespace Markdig.Syntax.Inlines +namespace Markdig.Syntax.Inlines; + +/// +/// An entity HTML. +/// +/// +[DebuggerDisplay("{Original} -> {Transcoded}")] +public class HtmlEntityInline : LeafInline { /// - /// An entity HTML. + /// Gets or sets the original HTML entity name /// - /// - [DebuggerDisplay("{Original} -> {Transcoded}")] - public class HtmlEntityInline : LeafInline - { - /// - /// Gets or sets the original HTML entity name - /// - public StringSlice Original { get; set; } + public StringSlice Original { get; set; } - /// - /// Gets or sets the transcoded literal that will be used for output - /// - public StringSlice Transcoded { get; set; } - } + /// + /// Gets or sets the transcoded literal that will be used for output + /// + public StringSlice Transcoded { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Syntax/Inlines/HtmlInline.cs b/src/Markdig/Syntax/Inlines/HtmlInline.cs index 538ee2d32..baa58cc7d 100644 --- a/src/Markdig/Syntax/Inlines/HtmlInline.cs +++ b/src/Markdig/Syntax/Inlines/HtmlInline.cs @@ -4,23 +4,22 @@ using System.Diagnostics; -namespace Markdig.Syntax.Inlines +namespace Markdig.Syntax.Inlines; + +/// +/// A Raw HTML (Section 6.8 CommonMark specs). +/// +/// +[DebuggerDisplay("{Tag}")] +public class HtmlInline : LeafInline { - /// - /// A Raw HTML (Section 6.8 CommonMark specs). - /// - /// - [DebuggerDisplay("{Tag}")] - public class HtmlInline : LeafInline + public HtmlInline(string tag) { - public HtmlInline(string tag) - { - Tag = tag; - } - - /// - /// Gets or sets the full declaration of this tag. - /// - public string Tag { get; set; } + Tag = tag; } + + /// + /// Gets or sets the full declaration of this tag. + /// + public string Tag { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Syntax/Inlines/IInline.cs b/src/Markdig/Syntax/Inlines/IInline.cs index a63478503..ec8e1e4e0 100644 --- a/src/Markdig/Syntax/Inlines/IInline.cs +++ b/src/Markdig/Syntax/Inlines/IInline.cs @@ -2,32 +2,31 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -namespace Markdig.Syntax.Inlines +namespace Markdig.Syntax.Inlines; + +/// +/// Base interface for all syntax tree inlines. +/// +/// +public interface IInline : IMarkdownObject { /// - /// Base interface for all syntax tree inlines. + /// Gets the parent container of this inline. /// - /// - public interface IInline : IMarkdownObject - { - /// - /// Gets the parent container of this inline. - /// - ContainerInline? Parent { get; } + ContainerInline? Parent { get; } - /// - /// Gets the previous inline. - /// - Inline? PreviousSibling { get; } + /// + /// Gets the previous inline. + /// + Inline? PreviousSibling { get; } - /// - /// Gets the next sibling inline. - /// - Inline? NextSibling { get; } + /// + /// Gets the next sibling inline. + /// + Inline? NextSibling { get; } - /// - /// Gets or sets a value indicating whether this instance is closed. - /// - bool IsClosed { get; set; } - } + /// + /// Gets or sets a value indicating whether this instance is closed. + /// + bool IsClosed { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Syntax/Inlines/Inline.cs b/src/Markdig/Syntax/Inlines/Inline.cs index 1747228b6..785c5ee14 100644 --- a/src/Markdig/Syntax/Inlines/Inline.cs +++ b/src/Markdig/Syntax/Inlines/Inline.cs @@ -2,315 +2,313 @@ // 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.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; + using Markdig.Helpers; -namespace Markdig.Syntax.Inlines +namespace Markdig.Syntax.Inlines; + +/// +/// Base class for all syntax tree inlines. +/// +/// +public abstract class Inline : MarkdownObject, IInline { + protected Inline() + { + SetTypeKind(isInline: true, isContainer: false); + } + /// - /// Base class for all syntax tree inlines. + /// Gets the parent container of this inline. /// - /// - public abstract class Inline : MarkdownObject, IInline + public ContainerInline? Parent { get; internal set; } + + /// + /// Gets the previous inline. + /// + public Inline? PreviousSibling { get; private set; } + + /// + /// Gets the next sibling inline. + /// + public Inline? NextSibling { get; internal set; } + + /// + /// Gets or sets a value indicating whether this instance is closed. + /// + public bool IsClosed { - protected Inline() - { - SetTypeKind(isInline: true, isContainer: false); - } + get => IsClosedInternal; + set => IsClosedInternal = value; + } - /// - /// Gets the parent container of this inline. - /// - public ContainerInline? Parent { get; internal set; } - - /// - /// Gets the previous inline. - /// - public Inline? PreviousSibling { get; private set; } - - /// - /// Gets the next sibling inline. - /// - public Inline? NextSibling { get; internal set; } - - /// - /// Gets or sets a value indicating whether this instance is closed. - /// - public bool IsClosed + /// + /// Inserts the specified inline after this instance. + /// + /// The inline to insert after this instance. + /// + /// Inline has already a parent + public void InsertAfter(Inline next) + { + if (next is null) ThrowHelper.ArgumentNullException(nameof(next)); + if (next.Parent != null) { - get => IsClosedInternal; - set => IsClosedInternal = value; + ThrowHelper.ArgumentException("Inline has already a parent", nameof(next)); } - /// - /// Inserts the specified inline after this instance. - /// - /// The inline to insert after this instance. - /// - /// Inline has already a parent - public void InsertAfter(Inline next) + var previousNext = NextSibling; + if (previousNext != null) { - if (next is null) ThrowHelper.ArgumentNullException(nameof(next)); - if (next.Parent != null) - { - ThrowHelper.ArgumentException("Inline has already a parent", nameof(next)); - } - - var previousNext = NextSibling; - if (previousNext != null) - { - previousNext.PreviousSibling = next; - } + previousNext.PreviousSibling = next; + } - next.PreviousSibling = this; - next.NextSibling = previousNext; - NextSibling = next; + next.PreviousSibling = this; + next.NextSibling = previousNext; + NextSibling = next; - if (Parent != null) - { - Parent.OnChildInsert(next); - next.Parent = Parent; - } + if (Parent != null) + { + Parent.OnChildInsert(next); + next.Parent = Parent; } + } - /// - /// Inserts the specified inline before this instance. - /// - /// The inline previous to insert before this instance. - /// - /// Inline has already a parent - public void InsertBefore(Inline previous) + /// + /// Inserts the specified inline before this instance. + /// + /// The inline previous to insert before this instance. + /// + /// Inline has already a parent + public void InsertBefore(Inline previous) + { + if (previous is null) ThrowHelper.ArgumentNullException(nameof(previous)); + if (previous.Parent != null) { - if (previous is null) ThrowHelper.ArgumentNullException(nameof(previous)); - if (previous.Parent != null) - { - ThrowHelper.ArgumentException("Inline has already a parent", nameof(previous)); - } + ThrowHelper.ArgumentException("Inline has already a parent", nameof(previous)); + } - var previousSibling = PreviousSibling; - if (previousSibling != null) - { - previousSibling.NextSibling = previous; - } + var previousSibling = PreviousSibling; + if (previousSibling != null) + { + previousSibling.NextSibling = previous; + } - PreviousSibling = previous; - previous.NextSibling = this; + PreviousSibling = previous; + previous.NextSibling = this; - if (Parent != null) - { - Parent.OnChildInsert(previous); - previous.Parent = Parent; - } + if (Parent != null) + { + Parent.OnChildInsert(previous); + previous.Parent = Parent; } + } - /// - /// Removes this instance from the current list and its parent - /// - public void Remove() + /// + /// Removes this instance from the current list and its parent + /// + public void Remove() + { + if (PreviousSibling != null) { - if (PreviousSibling != null) - { - PreviousSibling.NextSibling = NextSibling; - } + PreviousSibling.NextSibling = NextSibling; + } - if (NextSibling != null) - { - NextSibling.PreviousSibling = PreviousSibling; - } + if (NextSibling != null) + { + NextSibling.PreviousSibling = PreviousSibling; + } - if (Parent != null) - { - Parent.OnChildRemove(this); + if (Parent != null) + { + Parent.OnChildRemove(this); - PreviousSibling = null; - NextSibling = null; - Parent = null; - } + PreviousSibling = null; + NextSibling = null; + Parent = null; } + } - /// - /// Replaces this inline by the specified inline. - /// - /// The inline. - /// if set to true the children of this instance are copied to the specified inline. - /// The last children - /// If inline is null - public Inline ReplaceBy(Inline inline, bool copyChildren = true) - { - if (inline is null) ThrowHelper.ArgumentNullException(nameof(inline)); + /// + /// Replaces this inline by the specified inline. + /// + /// The inline. + /// if set to true the children of this instance are copied to the specified inline. + /// The last children + /// If inline is null + public Inline ReplaceBy(Inline inline, bool copyChildren = true) + { + if (inline is null) ThrowHelper.ArgumentNullException(nameof(inline)); - // Save sibling - var parent = Parent; - var previousSibling = PreviousSibling; - var nextSibling = NextSibling; - Remove(); + // Save sibling + var parent = Parent; + var previousSibling = PreviousSibling; + var nextSibling = NextSibling; + Remove(); - if (previousSibling != null) - { - previousSibling.InsertAfter(inline); - } - else if (nextSibling != null) - { - nextSibling.InsertBefore(inline); - } - else if (parent != null) - { - parent.AppendChild(inline); - } + if (previousSibling != null) + { + previousSibling.InsertAfter(inline); + } + else if (nextSibling != null) + { + nextSibling.InsertBefore(inline); + } + else if (parent != null) + { + parent.AppendChild(inline); + } - if (copyChildren && IsContainerInline) - { - var container = Unsafe.As(this); + if (copyChildren && IsContainerInline) + { + var container = Unsafe.As(this); - ContainerInline? newContainer = inline.IsContainerInline && !inline.IsClosed - ? Unsafe.As(inline) - : null; + ContainerInline? newContainer = inline.IsContainerInline && !inline.IsClosed + ? Unsafe.As(inline) + : null; - // TODO: This part is not efficient as it is using child.Remove() - // We need a method to quickly move all children without having to mess Next/Prev sibling - var child = container.FirstChild; - var lastChild = inline; - while (child != null) + // TODO: This part is not efficient as it is using child.Remove() + // We need a method to quickly move all children without having to mess Next/Prev sibling + var child = container.FirstChild; + var lastChild = inline; + while (child != null) + { + var nextChild = child.NextSibling; + child.Remove(); + if (newContainer != null) { - var nextChild = child.NextSibling; - child.Remove(); - if (newContainer != null) - { - newContainer.AppendChild(child); - } - else - { - lastChild.InsertAfter(child); - } - lastChild = child; - child = nextChild; + newContainer.AppendChild(child); } - - return lastChild; + else + { + lastChild.InsertAfter(child); + } + lastChild = child; + child = nextChild; } - return inline; + return lastChild; } - /// - /// Determines whether this instance contains a parent of the specified type. - /// - /// Type of the parent to check - /// true if this instance contains a parent of the specified type; false otherwise - public bool ContainsParentOfType() where T : Inline + return inline; + } + + /// + /// Determines whether this instance contains a parent of the specified type. + /// + /// Type of the parent to check + /// true if this instance contains a parent of the specified type; false otherwise + public bool ContainsParentOfType() where T : Inline + { + var inline = this; + while (inline != null) { - var inline = this; - while (inline != null) + var delimiter = inline as T; + if (delimiter != null) { - var delimiter = inline as T; - if (delimiter != null) - { - return true; - } - inline = inline.Parent; + return true; } - return false; + inline = inline.Parent; } + return false; + } - /// - /// Iterates on parents of the specified type. - /// - /// Type of the parent to iterate over - /// An enumeration on the parents of the specified type - public IEnumerable FindParentOfType() where T : Inline + /// + /// Iterates on parents of the specified type. + /// + /// Type of the parent to iterate over + /// An enumeration on the parents of the specified type + public IEnumerable FindParentOfType() where T : Inline + { + var inline = this; + while (inline != null) { - var inline = this; - while (inline != null) + if (inline is T inlineOfT) { - if (inline is T inlineOfT) - { - yield return inlineOfT; - } - inline = inline.Parent; + yield return inlineOfT; } + inline = inline.Parent; } + } - public T? FirstParentOfType() where T : notnull, Inline + public T? FirstParentOfType() where T : notnull, Inline + { + var inline = this; + while (inline != null) { - var inline = this; - while (inline != null) + if (inline is T inlineOfT) { - if (inline is T inlineOfT) - { - return inlineOfT; - } - inline = inline.Parent; + return inlineOfT; } - return null; + inline = inline.Parent; } + return null; + } - public Inline FindBestParent() - { - var current = this; + public Inline FindBestParent() + { + var current = this; - while (current.Parent != null || current.PreviousSibling != null) + while (current.Parent != null || current.PreviousSibling != null) + { + if (current.Parent != null) { - if (current.Parent != null) - { - current = current.Parent; - continue; - } - - current = current.PreviousSibling!; + current = current.Parent; + continue; } - return current; + current = current.PreviousSibling!; } - protected virtual void OnChildRemove(Inline child) - { + return current; + } - } + protected virtual void OnChildRemove(Inline child) + { - protected virtual void OnChildInsert(Inline child) - { - } + } - /// - /// Dumps this instance to . - /// - /// The writer. - /// - public void DumpTo(TextWriter writer) - { - if (writer is null) ThrowHelper.ArgumentNullException_writer(); - DumpTo(writer, 0); - } + protected virtual void OnChildInsert(Inline child) + { + } - /// - /// Dumps this instance to . - /// - /// The writer. - /// The level of indent. - /// if writer is null - public void DumpTo(TextWriter writer, int level) - { - if (writer is null) ThrowHelper.ArgumentNullException_writer(); - for (int i = 0; i < level; i++) - { - writer.Write(' '); - } + /// + /// Dumps this instance to . + /// + /// The writer. + /// + public void DumpTo(TextWriter writer) + { + if (writer is null) ThrowHelper.ArgumentNullException_writer(); + DumpTo(writer, 0); + } - writer.WriteLine("-> " + this.GetType().Name + " = " + this); + /// + /// Dumps this instance to . + /// + /// The writer. + /// The level of indent. + /// if writer is null + public void DumpTo(TextWriter writer, int level) + { + if (writer is null) ThrowHelper.ArgumentNullException_writer(); + for (int i = 0; i < level; i++) + { + writer.Write(' '); + } - DumpChildTo(writer, level + 1); + writer.WriteLine("-> " + this.GetType().Name + " = " + this); - if (NextSibling != null) - { - NextSibling.DumpTo(writer, level); - } - } + DumpChildTo(writer, level + 1); - protected virtual void DumpChildTo(TextWriter writer, int level) + if (NextSibling != null) { + NextSibling.DumpTo(writer, level); } } + + protected virtual void DumpChildTo(TextWriter writer, int level) + { + } } \ No newline at end of file diff --git a/src/Markdig/Syntax/Inlines/LeafInline.cs b/src/Markdig/Syntax/Inlines/LeafInline.cs index 4bb73f108..3cedae44b 100644 --- a/src/Markdig/Syntax/Inlines/LeafInline.cs +++ b/src/Markdig/Syntax/Inlines/LeafInline.cs @@ -2,13 +2,12 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -namespace Markdig.Syntax.Inlines +namespace Markdig.Syntax.Inlines; + +/// +/// A base class for a leaf inline. +/// +/// +public abstract class LeafInline : Inline { - /// - /// A base class for a leaf inline. - /// - /// - public abstract class LeafInline : Inline - { - } } \ No newline at end of file diff --git a/src/Markdig/Syntax/Inlines/LineBreakInline.cs b/src/Markdig/Syntax/Inlines/LineBreakInline.cs index 139c09a95..0abafa308 100644 --- a/src/Markdig/Syntax/Inlines/LineBreakInline.cs +++ b/src/Markdig/Syntax/Inlines/LineBreakInline.cs @@ -4,18 +4,17 @@ using Markdig.Helpers; -namespace Markdig.Syntax.Inlines +namespace Markdig.Syntax.Inlines; + +/// +/// A base class for a line break. +/// +/// +public class LineBreakInline : LeafInline { - /// - /// A base class for a line break. - /// - /// - public class LineBreakInline : LeafInline - { - public bool IsHard { get; set; } + public bool IsHard { get; set; } - public bool IsBackslash { get; set; } + public bool IsBackslash { get; set; } - public NewLine NewLine { get; set; } - } + public NewLine NewLine { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Syntax/Inlines/LinkDelimiterInline.cs b/src/Markdig/Syntax/Inlines/LinkDelimiterInline.cs index c954dc49e..7b91061f0 100644 --- a/src/Markdig/Syntax/Inlines/LinkDelimiterInline.cs +++ b/src/Markdig/Syntax/Inlines/LinkDelimiterInline.cs @@ -5,51 +5,50 @@ using Markdig.Helpers; using Markdig.Parsers; -namespace Markdig.Syntax.Inlines +namespace Markdig.Syntax.Inlines; + +/// +/// A delimiter for a link. +/// +/// +public class LinkDelimiterInline : DelimiterInline { + private TriviaProperties? _trivia => GetTrivia(); + private TriviaProperties Trivia => GetOrSetTrivia(); + + public LinkDelimiterInline(InlineParser parser) : base(parser) + { + } + + /// + /// Gets or sets a value indicating whether this delimiter is an image link. + /// + public bool IsImage { get; set; } + + /// + /// Gets or sets the label of this link. + /// + public string? Label { get; set; } + + /// + /// The label span + /// + public SourceSpan LabelSpan; + /// - /// A delimiter for a link. + /// Gets or sets the with trivia. + /// Trivia: only parsed when is enabled, otherwise + /// . /// - /// - public class LinkDelimiterInline : DelimiterInline + public StringSlice LabelWithTrivia { get => _trivia?.LabelWithTrivia ?? StringSlice.Empty; set => Trivia.LabelWithTrivia = value; } + + public override string ToLiteral() + { + return IsImage ? "![" : "["; + } + + private sealed class TriviaProperties { - private TriviaProperties? _trivia => GetTrivia(); - private TriviaProperties Trivia => GetOrSetTrivia(); - - public LinkDelimiterInline(InlineParser parser) : base(parser) - { - } - - /// - /// Gets or sets a value indicating whether this delimiter is an image link. - /// - public bool IsImage { get; set; } - - /// - /// Gets or sets the label of this link. - /// - public string? Label { get; set; } - - /// - /// The label span - /// - public SourceSpan LabelSpan; - - /// - /// Gets or sets the with trivia. - /// Trivia: only parsed when is enabled, otherwise - /// . - /// - 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; - } + public StringSlice LabelWithTrivia; } } \ No newline at end of file diff --git a/src/Markdig/Syntax/Inlines/LinkInline.cs b/src/Markdig/Syntax/Inlines/LinkInline.cs index 69d24958e..16ba75c35 100644 --- a/src/Markdig/Syntax/Inlines/LinkInline.cs +++ b/src/Markdig/Syntax/Inlines/LinkInline.cs @@ -5,195 +5,194 @@ using Markdig.Helpers; using System.Diagnostics; -namespace Markdig.Syntax.Inlines +namespace Markdig.Syntax.Inlines; + +public enum LocalLabel : byte { - public enum LocalLabel : byte + Local, // [foo][bar] + Empty, // [foo][] + None, // [foo] +} + +/// +/// A Link inline (Section 6.5 CommonMark specs) +/// +/// +[DebuggerDisplay("Url: {Url} Title: {Title} Image: {IsImage}")] +public class LinkInline : ContainerInline +{ + private TriviaProperties? _trivia => GetTrivia(); + private TriviaProperties Trivia => GetOrSetTrivia(); + + /// + /// A delegate to use if it is setup on this instance to allow late binding + /// of a Url. + /// + /// + public delegate string GetUrlDelegate(); + + /// + /// Initializes a new instance of the class. + /// + public LinkInline() { - Local, // [foo][bar] - Empty, // [foo][] - None, // [foo] } /// - /// A Link inline (Section 6.5 CommonMark specs) + /// Initializes a new instance of the class. + /// + /// The URL. + /// The title. + public LinkInline(string url, string title) + { + Url = url; + Title = title; + } + + /// + /// Gets or sets a value indicating whether this instance is an image link. + /// + public bool IsImage { get; set; } + + /// + /// Gets or sets the label. + /// + public string? Label { get; set; } + + /// + /// The label span + /// + public SourceSpan LabelSpan; + + /// + /// Gets or sets the with trivia. + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + public StringSlice LabelWithTrivia { get => _trivia?.LabelWithTrivia ?? StringSlice.Empty; set => Trivia.LabelWithTrivia = value; } + + /// + /// Gets or sets the type of label parsed + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + public LocalLabel LocalLabel { get => _trivia?.LocalLabel ?? LocalLabel.None; set => Trivia.LocalLabel = value; } + + /// + /// Gets or sets the reference this link is attached to. May be null. /// - /// - [DebuggerDisplay("Url: {Url} Title: {Title} Image: {IsImage}")] - public class LinkInline : ContainerInline + public LinkReferenceDefinition? Reference { get; set; } + + /// + /// Gets or sets the label as matched against the . + /// Trivia: only parsed when is enabled. + /// + public string? LinkRefDefLabel { get => _trivia?.LinkRefDefLabel; set => Trivia.LinkRefDefLabel = value; } + + /// + /// Gets or sets the with trivia as matched against + /// the + /// + public StringSlice LinkRefDefLabelWithTrivia { get => _trivia?.LinkRefDefLabelWithTrivia ?? StringSlice.Empty; set => Trivia.LinkRefDefLabelWithTrivia = value; } + + /// + /// Gets or sets the trivia before the . + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + public StringSlice TriviaBeforeUrl { get => _trivia?.TriviaBeforeUrl ?? StringSlice.Empty; set => Trivia.TriviaBeforeUrl = value; } + + /// + /// True if the in the source document is enclosed + /// in pointy brackets. + /// Trivia: only parsed when is enabled, otherwise + /// false. + /// + public bool UrlHasPointyBrackets { get => _trivia?.UrlHasPointyBrackets ?? false; set => Trivia.UrlHasPointyBrackets = value; } + + /// + /// Gets or sets the URL. + /// + public string? Url { get; set; } + + /// + /// The URL source span. + /// + public SourceSpan UrlSpan; + + /// + /// The but with trivia and unescaped characters + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + public StringSlice UnescapedUrl { get => _trivia?.UnescapedUrl ?? StringSlice.Empty; set => Trivia.UnescapedUrl = value; } + + /// + /// Any trivia after the . + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + public StringSlice TriviaAfterUrl { get => _trivia?.TriviaAfterUrl ?? StringSlice.Empty; set => Trivia.TriviaAfterUrl = value; } + + /// + /// Gets or sets the GetDynamicUrl delegate. If this property is set, + /// it is used instead of to get the Url from this instance. + /// + public GetUrlDelegate? GetDynamicUrl { get; set; } + + /// + /// Gets or sets the character used to enclose the . + /// Trivia: only parsed when is enabled. + /// + public char TitleEnclosingCharacter { get => _trivia?.TitleEnclosingCharacter ?? default; set => Trivia.TitleEnclosingCharacter = value; } + + /// + /// Gets or sets the title. + /// + public string? Title { get; set; } + + /// + /// The title source span. + /// + public SourceSpan TitleSpan; + + /// + /// Gets or sets the exactly as parsed from the + /// source document including unescaped characters + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + public StringSlice UnescapedTitle { get => _trivia?.UnescapedTitle ?? StringSlice.Empty; set => Trivia.UnescapedTitle = value; } + + /// + /// Gets or sets the trivia after the . + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + public StringSlice TriviaAfterTitle { get => _trivia?.TriviaAfterTitle ?? StringSlice.Empty; set => Trivia.TriviaAfterTitle = value; } + + /// + /// Gets or sets a boolean indicating if this link is a shortcut link to a + /// + public bool IsShortcut { get; set; } + + /// + /// Gets or sets a boolean indicating whether the inline link was parsed using markdown syntax or was automatic recognized. + /// + public bool IsAutoLink { get; set; } + + private sealed class TriviaProperties { - private TriviaProperties? _trivia => GetTrivia(); - private TriviaProperties Trivia => GetOrSetTrivia(); - - /// - /// A delegate to use if it is setup on this instance to allow late binding - /// of a Url. - /// - /// - public delegate string GetUrlDelegate(); - - /// - /// Initializes a new instance of the class. - /// - public LinkInline() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The URL. - /// The title. - public LinkInline(string url, string title) - { - Url = url; - Title = title; - } - - /// - /// Gets or sets a value indicating whether this instance is an image link. - /// - public bool IsImage { get; set; } - - /// - /// Gets or sets the label. - /// - public string? Label { get; set; } - - /// - /// The label span - /// - public SourceSpan LabelSpan; - - /// - /// Gets or sets the with trivia. - /// Trivia: only parsed when is enabled, otherwise - /// . - /// - public StringSlice LabelWithTrivia { get => _trivia?.LabelWithTrivia ?? StringSlice.Empty; set => Trivia.LabelWithTrivia = value; } - - /// - /// Gets or sets the type of label parsed - /// Trivia: only parsed when is enabled, otherwise - /// . - /// - public LocalLabel LocalLabel { get => _trivia?.LocalLabel ?? LocalLabel.None; set => Trivia.LocalLabel = value; } - - /// - /// Gets or sets the reference this link is attached to. May be null. - /// - public LinkReferenceDefinition? Reference { get; set; } - - /// - /// Gets or sets the label as matched against the . - /// Trivia: only parsed when is enabled. - /// - public string? LinkRefDefLabel { get => _trivia?.LinkRefDefLabel; set => Trivia.LinkRefDefLabel = value; } - - /// - /// Gets or sets the with trivia as matched against - /// the - /// - public StringSlice LinkRefDefLabelWithTrivia { get => _trivia?.LinkRefDefLabelWithTrivia ?? StringSlice.Empty; set => Trivia.LinkRefDefLabelWithTrivia = value; } - - /// - /// Gets or sets the trivia before the . - /// Trivia: only parsed when is enabled, otherwise - /// . - /// - public StringSlice TriviaBeforeUrl { get => _trivia?.TriviaBeforeUrl ?? StringSlice.Empty; set => Trivia.TriviaBeforeUrl = value; } - - /// - /// True if the in the source document is enclosed - /// in pointy brackets. - /// Trivia: only parsed when is enabled, otherwise - /// false. - /// - public bool UrlHasPointyBrackets { get => _trivia?.UrlHasPointyBrackets ?? false; set => Trivia.UrlHasPointyBrackets = value; } - - /// - /// Gets or sets the URL. - /// - public string? Url { get; set; } - - /// - /// The URL source span. - /// - public SourceSpan UrlSpan; - - /// - /// The but with trivia and unescaped characters - /// Trivia: only parsed when is enabled, otherwise - /// . - /// - public StringSlice UnescapedUrl { get => _trivia?.UnescapedUrl ?? StringSlice.Empty; set => Trivia.UnescapedUrl = value; } - - /// - /// Any trivia after the . - /// Trivia: only parsed when is enabled, otherwise - /// . - /// - public StringSlice TriviaAfterUrl { get => _trivia?.TriviaAfterUrl ?? StringSlice.Empty; set => Trivia.TriviaAfterUrl = value; } - - /// - /// Gets or sets the GetDynamicUrl delegate. If this property is set, - /// it is used instead of to get the Url from this instance. - /// - public GetUrlDelegate? GetDynamicUrl { get; set; } - - /// - /// Gets or sets the character used to enclose the . - /// Trivia: only parsed when is enabled. - /// - public char TitleEnclosingCharacter { get => _trivia?.TitleEnclosingCharacter ?? default; set => Trivia.TitleEnclosingCharacter = value; } - - /// - /// Gets or sets the title. - /// - public string? Title { get; set; } - - /// - /// The title source span. - /// - public SourceSpan TitleSpan; - - /// - /// Gets or sets the exactly as parsed from the - /// source document including unescaped characters - /// Trivia: only parsed when is enabled, otherwise - /// . - /// - public StringSlice UnescapedTitle { get => _trivia?.UnescapedTitle ?? StringSlice.Empty; set => Trivia.UnescapedTitle = value; } - - /// - /// Gets or sets the trivia after the . - /// Trivia: only parsed when is enabled, otherwise - /// . - /// - public StringSlice TriviaAfterTitle { get => _trivia?.TriviaAfterTitle ?? StringSlice.Empty; set => Trivia.TriviaAfterTitle = value; } - - /// - /// Gets or sets a boolean indicating if this link is a shortcut link to a - /// - public bool IsShortcut { get; set; } - - /// - /// Gets or sets a boolean indicating whether the inline link was parsed using markdown syntax or was automatic recognized. - /// - 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; - } + 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; } } diff --git a/src/Markdig/Syntax/Inlines/LiteralInline.cs b/src/Markdig/Syntax/Inlines/LiteralInline.cs index 781787b6a..dba392f64 100644 --- a/src/Markdig/Syntax/Inlines/LiteralInline.cs +++ b/src/Markdig/Syntax/Inlines/LiteralInline.cs @@ -2,64 +2,63 @@ // 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 Markdig.Helpers; -namespace Markdig.Syntax.Inlines +namespace Markdig.Syntax.Inlines; + +/// +/// A literal inline. +/// +/// +[DebuggerDisplay("{Content}")] +public class LiteralInline : LeafInline { /// - /// A literal inline. + /// Initializes a new instance of the class. /// - /// - [DebuggerDisplay("{Content}")] - public class LiteralInline : LeafInline + public LiteralInline() { - /// - /// Initializes a new instance of the class. - /// - public LiteralInline() - { - Content = new StringSlice(null!); - } + Content = new StringSlice(null!); + } - /// - /// Initializes a new instance of the class. - /// - /// The content. - public LiteralInline(StringSlice content) - { - Content = content; - } + /// + /// Initializes a new instance of the class. + /// + /// The content. + public LiteralInline(StringSlice content) + { + Content = content; + } - /// - /// Initializes a new instance of the class. - /// - /// The text. - /// - public LiteralInline(string text) - { - if (text is null) ThrowHelper.ArgumentNullException_text(); - Content = new StringSlice(text); - } + /// + /// Initializes a new instance of the class. + /// + /// The text. + /// + public LiteralInline(string text) + { + if (text is null) ThrowHelper.ArgumentNullException_text(); + Content = new StringSlice(text); + } - /// - /// The content as a . - /// - public StringSlice Content; + /// + /// The content as a . + /// + public StringSlice Content; - /// - /// A boolean indicating whether the first character of this literal is escaped by `\`. - /// - public bool IsFirstCharacterEscaped - { - get => InternalSpareBit; - set => InternalSpareBit = value; - } + /// + /// A boolean indicating whether the first character of this literal is escaped by `\`. + /// + public bool IsFirstCharacterEscaped + { + get => InternalSpareBit; + set => InternalSpareBit = value; + } - public override string ToString() - { - return Content.ToString(); - } + public override string ToString() + { + return Content.ToString(); } } \ No newline at end of file diff --git a/src/Markdig/Syntax/LeafBlock.cs b/src/Markdig/Syntax/LeafBlock.cs index c7d1a5883..a842e86f7 100644 --- a/src/Markdig/Syntax/LeafBlock.cs +++ b/src/Markdig/Syntax/LeafBlock.cs @@ -7,95 +7,94 @@ using Markdig.Parsers; using Markdig.Syntax.Inlines; -namespace Markdig.Syntax +namespace Markdig.Syntax; + +/// +/// Base class for all leaf blocks. +/// +/// +[DebuggerDisplay("{GetType().Name} Line: {Line}, {Lines}")] +public abstract class LeafBlock : Block { + private ContainerInline? inline; + /// - /// Base class for all leaf blocks. + /// Initializes a new instance of the class. /// - /// - [DebuggerDisplay("{GetType().Name} Line: {Line}, {Lines}")] - public abstract class LeafBlock : Block + /// The parser used to create this block. + protected LeafBlock(BlockParser? parser) : base(parser) { - private ContainerInline? inline; - - /// - /// Initializes a new instance of the class. - /// - /// The parser used to create this block. - protected LeafBlock(BlockParser? parser) : base(parser) - { - IsLeafBlock = true; - } + IsLeafBlock = true; + } - /// - /// Gets or sets the string lines accumulated for this leaf block. - /// May be null after process inlines have occurred. - /// - public StringLineGroup Lines; + /// + /// Gets or sets the string lines accumulated for this leaf block. + /// May be null after process inlines have occurred. + /// + public StringLineGroup Lines; - /// - /// Gets or sets the inline syntax tree (may be null). - /// - public ContainerInline? Inline + /// + /// Gets or sets the inline syntax tree (may be null). + /// + public ContainerInline? Inline + { + get => inline; + set { - get => inline; - set + if (value != null) { - if (value != null) - { - if (value.Parent != null) - ThrowHelper.ArgumentException("Cannot add this inline as it as already attached to another container (inline.Parent != null)"); - - if (value.ParentBlock != null) - ThrowHelper.ArgumentException("Cannot add this inline as it as already attached to another container (inline.ParentBlock != null)"); + if (value.Parent != null) + ThrowHelper.ArgumentException("Cannot add this inline as it as already attached to another container (inline.Parent != null)"); - value.ParentBlock = this; - } + if (value.ParentBlock != null) + ThrowHelper.ArgumentException("Cannot add this inline as it as already attached to another container (inline.ParentBlock != null)"); - if (inline != null) - { - inline.ParentBlock = null; - } + value.ParentBlock = this; + } - inline = value; + if (inline != null) + { + inline.ParentBlock = null; } + + inline = value; } + } - /// - /// Gets or sets a value indicating whether must be processed - /// as inline into the property. - /// - public bool ProcessInlines { get; set; } + /// + /// Gets or sets a value indicating whether must be processed + /// as inline into the property. + /// + public bool ProcessInlines { get; set; } - /// - /// Appends the specified line to this instance. - /// - /// The slice. - /// The column. - /// The line. - /// - /// Whether to keep track of trivia such as whitespace, extra heading characters and unescaped string values. - public void AppendLine(ref StringSlice slice, int column, int line, int sourceLinePosition, bool trackTrivia) + /// + /// Appends the specified line to this instance. + /// + /// The slice. + /// The column. + /// The line. + /// + /// Whether to keep track of trivia such as whitespace, extra heading characters and unescaped string values. + public void AppendLine(ref StringSlice slice, int column, int line, int sourceLinePosition, bool trackTrivia) + { + if (Lines.Lines is null) { - if (Lines.Lines is null) - { - Lines = new StringLineGroup(4, ProcessInlines); - } + 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 + 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) && !trackTrivia) - { - // 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 + if (slice.CurrentChar == '\t' && CharHelper.IsAcrossTab(column) && !trackTrivia) + { + // 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 } } \ No newline at end of file diff --git a/src/Markdig/Syntax/LinkReferenceDefinition.cs b/src/Markdig/Syntax/LinkReferenceDefinition.cs index 321d07b53..056d91deb 100644 --- a/src/Markdig/Syntax/LinkReferenceDefinition.cs +++ b/src/Markdig/Syntax/LinkReferenceDefinition.cs @@ -8,238 +8,237 @@ using Markdig.Parsers; using Markdig.Syntax.Inlines; -namespace Markdig.Syntax +namespace Markdig.Syntax; + +/// +/// A link reference definition (Section 4.7 CommonMark specs) +/// +/// +public class LinkReferenceDefinition : LeafBlock { + private TriviaProperties? _trivia => TryGetDerivedTrivia(); + private TriviaProperties Trivia => GetOrSetDerivedTrivia(); + + /// + /// Creates an inline link for the specified . + /// + /// State of the inline. + /// The link reference. + /// The child. + /// An inline link or null to use the default implementation + public delegate Inline CreateLinkInlineDelegate(InlineProcessor inlineState, LinkReferenceDefinition linkRef, Inline? child = null); + /// - /// A link reference definition (Section 4.7 CommonMark specs) + /// Initializes a new instance of the class. /// - /// - public class LinkReferenceDefinition : LeafBlock + public LinkReferenceDefinition() : base(null) { - private TriviaProperties? _trivia => TryGetDerivedTrivia(); - private TriviaProperties Trivia => GetOrSetDerivedTrivia(); - - /// - /// Creates an inline link for the specified . - /// - /// State of the inline. - /// The link reference. - /// The child. - /// An inline link or null to use the default implementation - public delegate Inline CreateLinkInlineDelegate(InlineProcessor inlineState, LinkReferenceDefinition linkRef, Inline? child = null); - - /// - /// Initializes a new instance of the class. - /// - public LinkReferenceDefinition() : base(null) - { - IsOpen = false; - } + IsOpen = false; + } + + /// + /// Initializes a new instance of the class. + /// + /// The label. + /// The URL. + /// The title. + public LinkReferenceDefinition(string? label, string? url, string? title) : this() + { + Label = label; + Url = url; + Title = title; + } - /// - /// Initializes a new instance of the class. - /// - /// The label. - /// The URL. - /// The title. - public LinkReferenceDefinition(string? label, string? url, string? title) : this() + /// + /// Gets or sets the label. Text is normalized according to spec. + /// + /// https://spec.commonmark.org/0.29/#matches + public string? Label { get; set; } + + /// + /// The label span + /// + public SourceSpan LabelSpan; + + /// + /// Non-normalized Label (includes trivia) + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + public StringSlice LabelWithTrivia { get => _trivia?.LabelWithTrivia ?? StringSlice.Empty; set => Trivia.LabelWithTrivia = value; } + + /// + /// Whitespace before the . + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + public StringSlice TriviaBeforeUrl { get => _trivia?.TriviaBeforeUrl ?? StringSlice.Empty; set => Trivia.TriviaBeforeUrl = value; } + + /// + /// Gets or sets the URL. + /// + public string? Url { get; set; } + + /// + /// The URL span + /// + public SourceSpan UrlSpan; + + /// + /// Non-normalized . + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + public StringSlice UnescapedUrl { get => _trivia?.UnescapedUrl ?? StringSlice.Empty; set => Trivia.UnescapedUrl = value; } + + /// + /// True when the is enclosed in point brackets in the source document. + /// Trivia: only parsed when is enabled, otherwise + /// false. + /// + public bool UrlHasPointyBrackets { get => _trivia?.UrlHasPointyBrackets ?? false; set => Trivia.UrlHasPointyBrackets = value; } + + /// + /// gets or sets the whitespace before a . + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + public StringSlice TriviaBeforeTitle { get => _trivia?.TriviaBeforeTitle ?? StringSlice.Empty; set => Trivia.TriviaBeforeTitle = value; } + + /// + /// Gets or sets the title. + /// + public string? Title { get; set; } + + /// + /// The title span + /// + public SourceSpan TitleSpan; + + /// + /// Non-normalized . + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + public StringSlice UnescapedTitle { get => _trivia?.UnescapedTitle ?? StringSlice.Empty; set => Trivia.UnescapedTitle = value; } + + /// + /// Gets or sets the character the is enclosed in. + /// Trivia: only parsed when is enabled, otherwise \0. + /// + public char TitleEnclosingCharacter { get => _trivia?.TitleEnclosingCharacter ?? default; set => Trivia.TitleEnclosingCharacter = value; } + + /// + /// Gets or sets the create link inline callback for this instance. + /// + /// + /// This callback is called when an inline link is matching this reference definition. + /// + public CreateLinkInlineDelegate? CreateLinkInline { get; set; } + + /// + /// Tries to the parse the specified text into a definition. + /// + /// Type of the text + /// The text. + /// The block. + /// true if parsing is successful; false otherwise + public static bool TryParse(ref T text, [NotNullWhen(true)] out LinkReferenceDefinition? block) where T : ICharIterator + { + block = null; + + var startSpan = text.Start; + + if (!LinkHelper.TryParseLinkReferenceDefinition(ref text, out string? label, out string? url, out string? title, out SourceSpan labelSpan, out SourceSpan urlSpan, out SourceSpan titleSpan)) { - Label = label; - Url = url; - Title = title; + return false; } - /// - /// Gets or sets the label. Text is normalized according to spec. - /// - /// https://spec.commonmark.org/0.29/#matches - public string? Label { get; set; } - - /// - /// The label span - /// - public SourceSpan LabelSpan; - - /// - /// Non-normalized Label (includes trivia) - /// Trivia: only parsed when is enabled, otherwise - /// . - /// - public StringSlice LabelWithTrivia { get => _trivia?.LabelWithTrivia ?? StringSlice.Empty; set => Trivia.LabelWithTrivia = value; } - - /// - /// Whitespace before the . - /// Trivia: only parsed when is enabled, otherwise - /// . - /// - public StringSlice TriviaBeforeUrl { get => _trivia?.TriviaBeforeUrl ?? StringSlice.Empty; set => Trivia.TriviaBeforeUrl = value; } - - /// - /// Gets or sets the URL. - /// - public string? Url { get; set; } - - /// - /// The URL span - /// - public SourceSpan UrlSpan; - - /// - /// Non-normalized . - /// Trivia: only parsed when is enabled, otherwise - /// . - /// - public StringSlice UnescapedUrl { get => _trivia?.UnescapedUrl ?? StringSlice.Empty; set => Trivia.UnescapedUrl = value; } - - /// - /// True when the is enclosed in point brackets in the source document. - /// Trivia: only parsed when is enabled, otherwise - /// false. - /// - public bool UrlHasPointyBrackets { get => _trivia?.UrlHasPointyBrackets ?? false; set => Trivia.UrlHasPointyBrackets = value; } - - /// - /// gets or sets the whitespace before a . - /// Trivia: only parsed when is enabled, otherwise - /// . - /// - public StringSlice TriviaBeforeTitle { get => _trivia?.TriviaBeforeTitle ?? StringSlice.Empty; set => Trivia.TriviaBeforeTitle = value; } - - /// - /// Gets or sets the title. - /// - public string? Title { get; set; } - - /// - /// The title span - /// - public SourceSpan TitleSpan; - - /// - /// Non-normalized . - /// Trivia: only parsed when is enabled, otherwise - /// . - /// - public StringSlice UnescapedTitle { get => _trivia?.UnescapedTitle ?? StringSlice.Empty; set => Trivia.UnescapedTitle = value; } - - /// - /// Gets or sets the character the is enclosed in. - /// Trivia: only parsed when is enabled, otherwise \0. - /// - public char TitleEnclosingCharacter { get => _trivia?.TitleEnclosingCharacter ?? default; set => Trivia.TitleEnclosingCharacter = value; } - - /// - /// Gets or sets the create link inline callback for this instance. - /// - /// - /// This callback is called when an inline link is matching this reference definition. - /// - public CreateLinkInlineDelegate? CreateLinkInline { get; set; } - - /// - /// Tries to the parse the specified text into a definition. - /// - /// Type of the text - /// The text. - /// The block. - /// true if parsing is successful; false otherwise - public static bool TryParse(ref T text, [NotNullWhen(true)] out LinkReferenceDefinition? block) where T : ICharIterator + block = new LinkReferenceDefinition(label, url, title) { - block = null; - - var startSpan = text.Start; - - if (!LinkHelper.TryParseLinkReferenceDefinition(ref text, out string? label, out string? url, out string? title, out SourceSpan labelSpan, out SourceSpan urlSpan, out SourceSpan titleSpan)) - { - return false; - } - - block = new LinkReferenceDefinition(label, url, title) - { - LabelSpan = labelSpan, - UrlSpan = urlSpan, - TitleSpan = titleSpan, - Span = new SourceSpan(startSpan, titleSpan.End > 0 ? titleSpan.End: urlSpan.End) - }; - return true; - } + LabelSpan = labelSpan, + UrlSpan = urlSpan, + TitleSpan = titleSpan, + Span = new SourceSpan(startSpan, titleSpan.End > 0 ? titleSpan.End: urlSpan.End) + }; + return true; + } - /// - /// Tries to the parse the specified text into a definition. - /// - /// Type of the text - /// The text. - /// The block. - /// - /// - /// - /// - /// - /// - /// - /// true if parsing is successful; false otherwise - public static bool TryParseTrivia( - ref T text, - [NotNullWhen(true)] out LinkReferenceDefinition? block, - out SourceSpan triviaBeforeLabel, - out SourceSpan labelWithTrivia, - out SourceSpan triviaBeforeUrl, - out SourceSpan unescapedUrl, - out SourceSpan triviaBeforeTitle, - out SourceSpan unescapedTitle, - out SourceSpan triviaAfterTitle) where T : ICharIterator + /// + /// Tries to the parse the specified text into a definition. + /// + /// Type of the text + /// The text. + /// The block. + /// + /// + /// + /// + /// + /// + /// + /// true if parsing is successful; false otherwise + public static bool TryParseTrivia( + ref T text, + [NotNullWhen(true)] out LinkReferenceDefinition? block, + out SourceSpan triviaBeforeLabel, + out SourceSpan labelWithTrivia, + out SourceSpan triviaBeforeUrl, + out SourceSpan unescapedUrl, + out SourceSpan triviaBeforeTitle, + out SourceSpan unescapedTitle, + out SourceSpan triviaAfterTitle) where T : ICharIterator + { + block = null; + + var startSpan = text.Start; + + if (!LinkHelper.TryParseLinkReferenceDefinitionTrivia( + ref text, + out triviaBeforeLabel, + out string? label, + out labelWithTrivia, + out triviaBeforeUrl, + out string? url, + out unescapedUrl, + out bool urlHasPointyBrackets, + out triviaBeforeTitle, + out string? title, + out unescapedTitle, + out char titleEnclosingCharacter, + out NewLine newLine, + out triviaAfterTitle, + out SourceSpan labelSpan, + out SourceSpan urlSpan, + out SourceSpan titleSpan)) { - block = null; - - var startSpan = text.Start; - - if (!LinkHelper.TryParseLinkReferenceDefinitionTrivia( - ref text, - out triviaBeforeLabel, - out string? label, - out labelWithTrivia, - out triviaBeforeUrl, - out string? url, - out unescapedUrl, - out bool urlHasPointyBrackets, - out triviaBeforeTitle, - out string? title, - out unescapedTitle, - out char titleEnclosingCharacter, - out NewLine newLine, - out triviaAfterTitle, - out SourceSpan labelSpan, - out SourceSpan urlSpan, - out SourceSpan titleSpan)) - { - return false; - } - - block = new LinkReferenceDefinition(label, url, title) - { - UrlHasPointyBrackets = urlHasPointyBrackets, - TitleEnclosingCharacter = titleEnclosingCharacter, - //LabelWithWhitespace = labelWithWhitespace, - LabelSpan = labelSpan, - UrlSpan = urlSpan, - //UnescapedUrl = unescapedUrl, - //UnescapedTitle = unescapedTitle, - TitleSpan = titleSpan, - Span = new SourceSpan(startSpan, titleSpan.End > 0 ? titleSpan.End : urlSpan.End), - NewLine = newLine, - }; - return true; + return false; } - private sealed class TriviaProperties + block = new LinkReferenceDefinition(label, url, title) { - public StringSlice LabelWithTrivia; - public StringSlice TriviaBeforeUrl; - public StringSlice UnescapedUrl; - public bool UrlHasPointyBrackets; - public StringSlice TriviaBeforeTitle; - public StringSlice UnescapedTitle; - public char TitleEnclosingCharacter; - } + UrlHasPointyBrackets = urlHasPointyBrackets, + TitleEnclosingCharacter = titleEnclosingCharacter, + //LabelWithWhitespace = labelWithWhitespace, + LabelSpan = labelSpan, + UrlSpan = urlSpan, + //UnescapedUrl = unescapedUrl, + //UnescapedTitle = unescapedTitle, + TitleSpan = titleSpan, + Span = new SourceSpan(startSpan, titleSpan.End > 0 ? titleSpan.End : urlSpan.End), + NewLine = newLine, + }; + 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; } } \ No newline at end of file diff --git a/src/Markdig/Syntax/LinkReferenceDefinitionExtensions.cs b/src/Markdig/Syntax/LinkReferenceDefinitionExtensions.cs index cb8d05d1f..12433407f 100644 --- a/src/Markdig/Syntax/LinkReferenceDefinitionExtensions.cs +++ b/src/Markdig/Syntax/LinkReferenceDefinitionExtensions.cs @@ -6,59 +6,58 @@ using Markdig.Helpers; -namespace Markdig.Syntax +namespace Markdig.Syntax; + +/// +/// Extension methods for accessing attached at the document level. +/// +public static class LinkReferenceDefinitionExtensions { - /// - /// Extension methods for accessing attached at the document level. - /// - public static class LinkReferenceDefinitionExtensions - { - private static readonly object DocumentKey = typeof(LinkReferenceDefinitionGroup); + private static readonly object DocumentKey = typeof(LinkReferenceDefinitionGroup); - public static bool ContainsLinkReferenceDefinition(this MarkdownDocument document, string label) + public static bool ContainsLinkReferenceDefinition(this MarkdownDocument document, string label) + { + if (label is null) ThrowHelper.ArgumentNullException_label(); + var references = document.GetData(DocumentKey) as LinkReferenceDefinitionGroup; + if (references is null) { - if (label is null) ThrowHelper.ArgumentNullException_label(); - var references = document.GetData(DocumentKey) as LinkReferenceDefinitionGroup; - if (references is null) - { - return false; - } - return references.Links.ContainsKey(label); + return false; } + return references.Links.ContainsKey(label); + } - public static void SetLinkReferenceDefinition(this MarkdownDocument document, string label, LinkReferenceDefinition linkReferenceDefinition, bool addGroup) - { - if (label is null) ThrowHelper.ArgumentNullException_label(); - var references = document.GetLinkReferenceDefinitions(addGroup); - references.Set(label, linkReferenceDefinition); - } + public static void SetLinkReferenceDefinition(this MarkdownDocument document, string label, LinkReferenceDefinition linkReferenceDefinition, bool addGroup) + { + if (label is null) ThrowHelper.ArgumentNullException_label(); + var references = document.GetLinkReferenceDefinitions(addGroup); + references.Set(label, linkReferenceDefinition); + } - public static bool TryGetLinkReferenceDefinition(this MarkdownDocument document, string label, [NotNullWhen(true)] out LinkReferenceDefinition? linkReferenceDefinition) + public static bool TryGetLinkReferenceDefinition(this MarkdownDocument document, string label, [NotNullWhen(true)] out LinkReferenceDefinition? linkReferenceDefinition) + { + if (label is null) ThrowHelper.ArgumentNullException_label(); + linkReferenceDefinition = null; + var references = document.GetData(DocumentKey) as LinkReferenceDefinitionGroup; + if (references is null) { - if (label is null) ThrowHelper.ArgumentNullException_label(); - linkReferenceDefinition = null; - var references = document.GetData(DocumentKey) as LinkReferenceDefinitionGroup; - if (references is null) - { - return false; - } - return references.TryGet(label, out linkReferenceDefinition); + return false; } + return references.TryGet(label, out linkReferenceDefinition); + } - public static LinkReferenceDefinitionGroup GetLinkReferenceDefinitions(this MarkdownDocument document, bool addGroup) + public static LinkReferenceDefinitionGroup GetLinkReferenceDefinitions(this MarkdownDocument document, bool addGroup) + { + var references = document.GetData(DocumentKey) as LinkReferenceDefinitionGroup; + if (references is null) { - var references = document.GetData(DocumentKey) as LinkReferenceDefinitionGroup; - if (references is null) + references = new LinkReferenceDefinitionGroup(); + document.SetData(DocumentKey, references); + // don't add the LinkReferenceDefinitionGroup when tracking trivia + if (addGroup) { - references = new LinkReferenceDefinitionGroup(); - document.SetData(DocumentKey, references); - // don't add the LinkReferenceDefinitionGroup when tracking trivia - if (addGroup) - { - document.Add(references); - } + document.Add(references); } - return references; } + return references; } } \ No newline at end of file diff --git a/src/Markdig/Syntax/LinkReferenceDefinitionGroup.cs b/src/Markdig/Syntax/LinkReferenceDefinitionGroup.cs index b93962bb9..9f79636a3 100644 --- a/src/Markdig/Syntax/LinkReferenceDefinitionGroup.cs +++ b/src/Markdig/Syntax/LinkReferenceDefinitionGroup.cs @@ -2,55 +2,53 @@ // 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.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; + using Markdig.Helpers; -namespace Markdig.Syntax +namespace Markdig.Syntax; + +/// +/// Contains all the found in a document. +/// +/// +public class LinkReferenceDefinitionGroup : ContainerBlock { - /// - /// Contains all the found in a document. - /// - /// - public class LinkReferenceDefinitionGroup : ContainerBlock - { #if NETFRAMEWORK - private static readonly StringComparer _unicodeIgnoreCaseComparer = StringComparer.InvariantCultureIgnoreCase; + private static readonly StringComparer _unicodeIgnoreCaseComparer = StringComparer.InvariantCultureIgnoreCase; #else - private static readonly StringComparer _unicodeIgnoreCaseComparer = CultureInfo.InvariantCulture.CompareInfo.GetStringComparer(CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace); + private static readonly StringComparer _unicodeIgnoreCaseComparer = CultureInfo.InvariantCulture.CompareInfo.GetStringComparer(CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace); #endif - /// - /// Initializes a new instance of the class. - /// - public LinkReferenceDefinitionGroup() : base(null) - { - Links = new Dictionary(_unicodeIgnoreCaseComparer); - } + /// + /// Initializes a new instance of the class. + /// + public LinkReferenceDefinitionGroup() : base(null) + { + Links = new Dictionary(_unicodeIgnoreCaseComparer); + } - /// - /// Gets an association between a label and the corresponding - /// - public Dictionary Links { get; } + /// + /// Gets an association between a label and the corresponding + /// + public Dictionary Links { get; } - public void Set(string label, LinkReferenceDefinition link) + public void Set(string label, LinkReferenceDefinition link) + { + if (link is null) ThrowHelper.ArgumentNullException(nameof(link)); + if (!Contains(link)) { - if (link is null) ThrowHelper.ArgumentNullException(nameof(link)); - if (!Contains(link)) + Add(link); + if (!Links.ContainsKey(label)) { - Add(link); - if (!Links.ContainsKey(label)) - { - Links[label] = link; - } + Links[label] = link; } } + } - public bool TryGet(string label, [NotNullWhen(true)] out LinkReferenceDefinition? link) - { - return Links.TryGetValue(label, out link); - } + public bool TryGet(string label, [NotNullWhen(true)] out LinkReferenceDefinition? link) + { + return Links.TryGetValue(label, out link); } } \ No newline at end of file diff --git a/src/Markdig/Syntax/ListBlock.cs b/src/Markdig/Syntax/ListBlock.cs index cd0a0b7bd..9e790bed7 100644 --- a/src/Markdig/Syntax/ListBlock.cs +++ b/src/Markdig/Syntax/ListBlock.cs @@ -4,54 +4,53 @@ using Markdig.Parsers; -namespace Markdig.Syntax +namespace Markdig.Syntax; + +/// +/// A list (Section 5.3 CommonMark specs) +/// +/// +public class ListBlock : ContainerBlock { /// - /// A list (Section 5.3 CommonMark specs) + /// Initializes a new instance of the class. /// - /// - public class ListBlock : ContainerBlock + /// The parser used to create this block. + public ListBlock(BlockParser parser) : base(parser) { - /// - /// Initializes a new instance of the class. - /// - /// The parser used to create this block. - public ListBlock(BlockParser parser) : base(parser) - { - } - - /// - /// Gets or sets a value indicating whether the list is ordered. - /// - public bool IsOrdered { get; set; } - - /// - /// Gets or sets the bullet character used by this list. - /// - public char BulletType { get; set; } - - /// - /// Gets or sets the ordered start number (valid when is true) - /// - public string? OrderedStart { get; set; } - - /// - /// Gets or sets the default ordered start ("1" for BulletType = '1') - /// - public string? DefaultOrderedStart { get; set; } - - /// - /// Gets or sets the ordered delimiter character (usually `.` or `)`) found after an ordered list item. - /// - public char OrderedDelimiter { get; set; } - - /// - /// Gets or sets a value indicating whether this instance is loose. - /// - public bool IsLoose { get; set; } - - internal int CountAllBlankLines { get; set; } - - internal int CountBlankLinesReset { get; set; } } + + /// + /// Gets or sets a value indicating whether the list is ordered. + /// + public bool IsOrdered { get; set; } + + /// + /// Gets or sets the bullet character used by this list. + /// + public char BulletType { get; set; } + + /// + /// Gets or sets the ordered start number (valid when is true) + /// + public string? OrderedStart { get; set; } + + /// + /// Gets or sets the default ordered start ("1" for BulletType = '1') + /// + public string? DefaultOrderedStart { get; set; } + + /// + /// Gets or sets the ordered delimiter character (usually `.` or `)`) found after an ordered list item. + /// + public char OrderedDelimiter { get; set; } + + /// + /// Gets or sets a value indicating whether this instance is loose. + /// + public bool IsLoose { get; set; } + + internal int CountAllBlankLines { get; set; } + + internal int CountBlankLinesReset { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Syntax/ListItemBlock.cs b/src/Markdig/Syntax/ListItemBlock.cs index dda923dfb..0f3f19172 100644 --- a/src/Markdig/Syntax/ListItemBlock.cs +++ b/src/Markdig/Syntax/ListItemBlock.cs @@ -5,42 +5,41 @@ using Markdig.Helpers; using Markdig.Parsers; -namespace Markdig.Syntax +namespace Markdig.Syntax; + +/// +/// A list item (Section 5.2 CommonMark specs) +/// +/// +public class ListItemBlock : ContainerBlock { + private TriviaProperties? _trivia => TryGetDerivedTrivia(); + private TriviaProperties Trivia => GetOrSetDerivedTrivia(); + + /// + /// Initializes a new instance of the class. + /// + /// The parser used to create this block. + public ListItemBlock(BlockParser parser) : base(parser) + { + } + + public int ColumnWidth { get; set; } + + /// + /// The number defined for this in an ordered list + /// + public int Order { get; set; } + /// - /// A list item (Section 5.2 CommonMark specs) + /// Gets or sets the bullet as parsed in the source document. + /// Trivia: only parsed when is enabled, otherwise + /// . /// - /// - public class ListItemBlock : ContainerBlock + public StringSlice SourceBullet { get => _trivia?.SourceBullet ?? StringSlice.Empty; set => Trivia.SourceBullet = value; } + + private sealed class TriviaProperties { - private TriviaProperties? _trivia => TryGetDerivedTrivia(); - private TriviaProperties Trivia => GetOrSetDerivedTrivia(); - - /// - /// Initializes a new instance of the class. - /// - /// The parser used to create this block. - public ListItemBlock(BlockParser parser) : base(parser) - { - } - - public int ColumnWidth { get; set; } - - /// - /// The number defined for this in an ordered list - /// - public int Order { get; set; } - - /// - /// Gets or sets the bullet as parsed in the source document. - /// Trivia: only parsed when is enabled, otherwise - /// . - /// - public StringSlice SourceBullet { get => _trivia?.SourceBullet ?? StringSlice.Empty; set => Trivia.SourceBullet = value; } - - private sealed class TriviaProperties - { - public StringSlice SourceBullet; - } + public StringSlice SourceBullet; } } \ No newline at end of file diff --git a/src/Markdig/Syntax/MarkdownDocument.cs b/src/Markdig/Syntax/MarkdownDocument.cs index 9bf9b0077..a29930d4c 100644 --- a/src/Markdig/Syntax/MarkdownDocument.cs +++ b/src/Markdig/Syntax/MarkdownDocument.cs @@ -2,32 +2,29 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using System.Collections.Generic; +namespace Markdig.Syntax; -namespace Markdig.Syntax +/// +/// The root Markdown document. +/// +/// +public class MarkdownDocument : ContainerBlock { /// - /// The root Markdown document. + /// Initializes a new instance of the class. /// - /// - public class MarkdownDocument : ContainerBlock + public MarkdownDocument() : base(null) { - /// - /// Initializes a new instance of the class. - /// - public MarkdownDocument() : base(null) - { - } + } - /// - /// Gets the number of lines in this - /// - public int LineCount; + /// + /// Gets the number of lines in this + /// + public int LineCount; - /// - /// Gets a list of zero-based indexes of line beginnings in the source span - /// Available if is used, otherwise null - /// - public List? LineStartIndexes { get; set; } - } + /// + /// Gets a list of zero-based indexes of line beginnings in the source span + /// Available if is used, otherwise null + /// + public List? LineStartIndexes { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Syntax/MarkdownObject.cs b/src/Markdig/Syntax/MarkdownObject.cs index 070bffb73..293697fa1 100644 --- a/src/Markdig/Syntax/MarkdownObject.cs +++ b/src/Markdig/Syntax/MarkdownObject.cs @@ -2,283 +2,282 @@ // 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.Runtime.CompilerServices; + using Markdig.Helpers; -namespace Markdig.Syntax +namespace Markdig.Syntax; + +/// +/// Base implementation for a the Markdown syntax tree. +/// +public abstract class MarkdownObject : IMarkdownObject { - /// - /// Base implementation for a the Markdown syntax tree. - /// - public abstract class MarkdownObject : IMarkdownObject - { - private const uint ValueBitMask = (1u << 30) - 1; - private const uint FirstBitMask = 1u << 31; - private const uint SecondBitMask = 1u << 30; + private const uint ValueBitMask = (1u << 30) - 1; + private const uint FirstBitMask = 1u << 31; + private const uint SecondBitMask = 1u << 30; + + private const uint IsInlineMask = FirstBitMask; + private const uint IsContainerMask = SecondBitMask; + private const uint TypeKindMask = IsInlineMask | IsContainerMask; - private const uint IsInlineMask = FirstBitMask; - private const uint IsContainerMask = SecondBitMask; - private const uint TypeKindMask = IsInlineMask | IsContainerMask; + // Limit the value to 30 bits and repurpose the last two bits for commonly used flags + private uint _lineBits; // Also stores TypeKindMask (IsInline and IsContainer) + private uint _columnBits; // Also stores IsClosedInternal and InternalSpareBit - // Limit the value to 30 bits and repurpose the last two bits for commonly used flags - private uint _lineBits; // Also stores TypeKindMask (IsInline and IsContainer) - private uint _columnBits; // Also stores IsClosedInternal and InternalSpareBit + internal bool IsContainerInline => (_lineBits & TypeKindMask) == (IsContainerMask | IsInlineMask); - internal bool IsContainerInline => (_lineBits & TypeKindMask) == (IsContainerMask | IsInlineMask); + internal bool IsContainerBlock => (_lineBits & TypeKindMask) == IsContainerMask; - internal bool IsContainerBlock => (_lineBits & TypeKindMask) == IsContainerMask; + internal bool IsContainer => (_lineBits & IsContainerMask) != 0; - internal bool IsContainer => (_lineBits & IsContainerMask) != 0; + internal bool IsInline => (_lineBits & IsInlineMask) != 0; - internal bool IsInline => (_lineBits & IsInlineMask) != 0; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private protected void SetTypeKind(bool isInline, bool isContainer) + { + _lineBits |= (isInline ? IsInlineMask : 0) | (isContainer ? IsContainerMask : 0); + } + private protected bool IsClosedInternal + { + get => (_columnBits & FirstBitMask) != 0; [MethodImpl(MethodImplOptions.AggressiveInlining)] - private protected void SetTypeKind(bool isInline, bool isContainer) + set { - _lineBits |= (isInline ? IsInlineMask : 0) | (isContainer ? IsContainerMask : 0); + if (value) _columnBits |= FirstBitMask; + else _columnBits &= ~FirstBitMask; } + } - private protected bool IsClosedInternal + private protected bool InternalSpareBit + { + get => (_columnBits & SecondBitMask) != 0; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set { - get => (_columnBits & FirstBitMask) != 0; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - set - { - if (value) _columnBits |= FirstBitMask; - else _columnBits &= ~FirstBitMask; - } + if (value) _columnBits |= SecondBitMask; + else _columnBits &= ~SecondBitMask; } + } - private protected bool InternalSpareBit - { - get => (_columnBits & SecondBitMask) != 0; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - set - { - if (value) _columnBits |= SecondBitMask; - else _columnBits &= ~SecondBitMask; - } - } + protected MarkdownObject() + { + Span = SourceSpan.Empty; + } - protected MarkdownObject() - { - Span = SourceSpan.Empty; - } + /// + /// The attached datas. Use internally a simple array instead of a Dictionary{Object,Object} + /// as we expect less than 5~10 entries, usually typically 1 (HtmlAttributes) + /// so it will gives faster access than a Dictionary, and lower memory occupation + /// + private DataEntriesAndTrivia? _attachedDatas; - /// - /// The attached datas. Use internally a simple array instead of a Dictionary{Object,Object} - /// as we expect less than 5~10 entries, usually typically 1 (HtmlAttributes) - /// so it will gives faster access than a Dictionary, and lower memory occupation - /// - private DataEntriesAndTrivia? _attachedDatas; - - /// - /// Gets or sets the text column this instance was declared (zero-based). - /// - public int Column - { - get => (int)(_columnBits & ValueBitMask); - set => _columnBits = (_columnBits & ~ValueBitMask) | ((uint)value & ValueBitMask); - } + /// + /// Gets or sets the text column this instance was declared (zero-based). + /// + public int Column + { + get => (int)(_columnBits & ValueBitMask); + set => _columnBits = (_columnBits & ~ValueBitMask) | ((uint)value & ValueBitMask); + } - /// - /// Gets or sets the text line this instance was declared (zero-based). - /// - public int Line - { - get => (int)(_lineBits & ValueBitMask); - set => _lineBits = (_lineBits & ~ValueBitMask) | ((uint)value & ValueBitMask); - } + /// + /// Gets or sets the text line this instance was declared (zero-based). + /// + public int Line + { + get => (int)(_lineBits & ValueBitMask); + set => _lineBits = (_lineBits & ~ValueBitMask) | ((uint)value & ValueBitMask); + } - /// - /// The source span - /// - public SourceSpan Span; + /// + /// The source span + /// + public SourceSpan Span; - /// - /// Gets a string of the location in the text. - /// - /// - public string ToPositionText() - { - return $"${Line}, {Column}, {Span.Start}-{Span.End}"; - } + /// + /// Gets a string of the location in the text. + /// + /// + public string ToPositionText() + { + return $"${Line}, {Column}, {Span.Start}-{Span.End}"; + } - /// - /// Stores a key/value pair for this instance. - /// - /// The key. - /// The value. - /// if key is null - public void SetData(object key, object value) => (_attachedDatas ??= new DataEntriesAndTrivia()).SetData(key, value); - - /// - /// Determines whether this instance contains the specified key data. - /// - /// The key. - /// true if a data with the key is stored - /// if key is null - public bool ContainsData(object key) => _attachedDatas?.ContainsData(key) ?? false; - - /// - /// Gets the associated data for the specified key. - /// - /// The key. - /// The associated data or null if none - /// if key is null - public object? GetData(object key) => _attachedDatas?.GetData(key); - - /// - /// Removes the associated data for the specified key. - /// - /// The key. - /// true if the data was removed; false otherwise - /// - public bool RemoveData(object key) => _attachedDatas?.RemoveData(key) ?? false; + /// + /// Stores a key/value pair for this instance. + /// + /// The key. + /// The value. + /// if key is null + public void SetData(object key, object value) => (_attachedDatas ??= new DataEntriesAndTrivia()).SetData(key, value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private protected T? GetTrivia() where T : class - { - object? trivia = _attachedDatas?.Trivia; - return trivia is null ? null : Unsafe.As(trivia); - } + /// + /// Determines whether this instance contains the specified key data. + /// + /// The key. + /// true if a data with the key is stored + /// if key is null + public bool ContainsData(object key) => _attachedDatas?.ContainsData(key) ?? false; - private protected T GetOrSetTrivia() where T : class, new() - { - var storage = _attachedDatas ??= new DataEntriesAndTrivia(); - storage.Trivia ??= new T(); - return Unsafe.As(storage.Trivia); - } + /// + /// Gets the associated data for the specified key. + /// + /// The key. + /// The associated data or null if none + /// if key is null + public object? GetData(object key) => _attachedDatas?.GetData(key); - private class DataEntriesAndTrivia - { - private struct DataEntry - { - public readonly object Key; - public object Value; + /// + /// Removes the associated data for the specified key. + /// + /// The key. + /// true if the data was removed; false otherwise + /// + public bool RemoveData(object key) => _attachedDatas?.RemoveData(key) ?? false; - public DataEntry(object key, object value) - { - Key = key; - Value = value; - } - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private protected T? GetTrivia() where T : class + { + object? trivia = _attachedDatas?.Trivia; + return trivia is null ? null : Unsafe.As(trivia); + } - private DataEntry[]? _entries; - private int _count; + private protected T GetOrSetTrivia() where T : class, new() + { + var storage = _attachedDatas ??= new DataEntriesAndTrivia(); + storage.Trivia ??= new T(); + return Unsafe.As(storage.Trivia); + } - public object? Trivia; + private class DataEntriesAndTrivia + { + private struct DataEntry + { + public readonly object Key; + public object Value; - public void SetData(object key, object value) + public DataEntry(object key, object value) { - if (key is null) ThrowHelper.ArgumentNullException_key(); + Key = key; + Value = value; + } + } - DataEntry[]? entries = _entries; - int count = _count; + private DataEntry[]? _entries; + private int _count; - if (entries is null) - { - _entries = new DataEntry[2]; - } - else - { - for (int i = 0; i < entries.Length && i < count; i++) - { - ref DataEntry entry = ref entries[i]; - if (entry.Key == key) - { - entry.Value = value; - return; - } - } + public object? Trivia; - if (count == entries.Length) - { - Array.Resize(ref _entries, count + 2); - } - } + public void SetData(object key, object value) + { + if (key is null) ThrowHelper.ArgumentNullException_key(); - _entries![count] = new DataEntry(key, value); - _count++; - } + DataEntry[]? entries = _entries; + int count = _count; - public object? GetData(object key) + if (entries is null) + { + _entries = new DataEntry[2]; + } + else { - if (key is null) ThrowHelper.ArgumentNullException_key(); - - DataEntry[]? entries = _entries; - if (entries is null) - { - return null; - } - - int count = _count; - for (int i = 0; i < entries.Length && i < count; i++) { ref DataEntry entry = ref entries[i]; if (entry.Key == key) { - return entry.Value; + entry.Value = value; + return; } } - return null; + if (count == entries.Length) + { + Array.Resize(ref _entries, count + 2); + } } - public bool ContainsData(object key) + _entries![count] = new DataEntry(key, value); + _count++; + } + + public object? GetData(object key) + { + if (key is null) ThrowHelper.ArgumentNullException_key(); + + DataEntry[]? entries = _entries; + if (entries is null) { - if (key is null) ThrowHelper.ArgumentNullException_key(); + return null; + } - DataEntry[]? entries = _entries; - if (entries is null) + int count = _count; + + for (int i = 0; i < entries.Length && i < count; i++) + { + ref DataEntry entry = ref entries[i]; + if (entry.Key == key) { - return false; + return entry.Value; } + } - int count = _count; + return null; + } - for (int i = 0; i < entries.Length && i < count; i++) - { - if (entries[i].Key == key) - { - return true; - } - } + public bool ContainsData(object key) + { + if (key is null) ThrowHelper.ArgumentNullException_key(); + DataEntry[]? entries = _entries; + if (entries is null) + { return false; } - public bool RemoveData(object key) - { - if (key is null) ThrowHelper.ArgumentNullException_key(); + int count = _count; - DataEntry[]? entries = _entries; - if (entries is null) + for (int i = 0; i < entries.Length && i < count; i++) + { + if (entries[i].Key == key) { - return false; + return true; } + } - int count = _count; + return false; + } - for (int i = 0; i < entries.Length && i < count; i++) + public bool RemoveData(object key) + { + if (key is null) ThrowHelper.ArgumentNullException_key(); + + DataEntry[]? entries = _entries; + if (entries is null) + { + return false; + } + + int count = _count; + + for (int i = 0; i < entries.Length && i < count; i++) + { + if (entries[i].Key == key) { - if (entries[i].Key == key) + if (i < count - 1) { - if (i < count - 1) - { - Array.Copy(entries, i + 1, entries, i, count - i - 1); - } - count--; - entries[count] = default; - _count = count; - return true; + Array.Copy(entries, i + 1, entries, i, count - i - 1); } + count--; + entries[count] = default; + _count = count; + return true; } - - return false; } + + return false; } } } \ No newline at end of file diff --git a/src/Markdig/Syntax/MarkdownObjectExtensions.cs b/src/Markdig/Syntax/MarkdownObjectExtensions.cs index fd39973b0..2b73bc84a 100644 --- a/src/Markdig/Syntax/MarkdownObjectExtensions.cs +++ b/src/Markdig/Syntax/MarkdownObjectExtensions.cs @@ -2,174 +2,171 @@ // 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.Collections.Generic; using System.Diagnostics; -using Markdig.Helpers; + using Markdig.Syntax.Inlines; -namespace Markdig.Syntax +namespace Markdig.Syntax; + +/// +/// Extensions for visiting or +/// +public static class MarkdownObjectExtensions { /// - /// Extensions for visiting or + /// Iterates over the descendant elements for the specified markdown element, including and . + /// The descendant elements are returned in DFS-like order. /// - public static class MarkdownObjectExtensions + /// The markdown object. + /// An iteration over the descendant elements + public static IEnumerable Descendants(this MarkdownObject markdownObject) { - /// - /// Iterates over the descendant elements for the specified markdown element, including and . - /// The descendant elements are returned in DFS-like order. - /// - /// The markdown object. - /// An iteration over the descendant elements - public static IEnumerable Descendants(this MarkdownObject markdownObject) - { - var stack = new Stack(); - var pushStack = new Stack(); + var stack = new Stack(); + var pushStack = new Stack(); - stack.Push(markdownObject); - pushStack.Push(false); + stack.Push(markdownObject); + pushStack.Push(false); - while (stack.Count > 0) + while (stack.Count > 0) + { + var block = stack.Pop(); + if (pushStack.Pop()) yield return block; + if (block is ContainerBlock containerBlock) { - var block = stack.Pop(); - if (pushStack.Pop()) yield return block; - if (block is ContainerBlock containerBlock) + int subBlockIndex = containerBlock.Count; + while (subBlockIndex-- > 0) { - int subBlockIndex = containerBlock.Count; - while (subBlockIndex-- > 0) + var subBlock = containerBlock[subBlockIndex]; + if (subBlock is LeafBlock leafBlock) { - var subBlock = containerBlock[subBlockIndex]; - if (subBlock is LeafBlock leafBlock) + if (leafBlock.Inline != null) { - if (leafBlock.Inline != null) - { - stack.Push(leafBlock.Inline); - pushStack.Push(false); - } + stack.Push(leafBlock.Inline); + pushStack.Push(false); } - stack.Push(subBlock); - pushStack.Push(true); } + stack.Push(subBlock); + pushStack.Push(true); } - else if (block is ContainerInline containerInline) + } + else if (block is ContainerInline containerInline) + { + var child = containerInline.LastChild; + while (child != null) { - var child = containerInline.LastChild; - while (child != null) - { - stack.Push(child); - pushStack.Push(true); - child = child.PreviousSibling; - } + stack.Push(child); + pushStack.Push(true); + child = child.PreviousSibling; } } } + } - /// - /// Iterates over the descendant elements for the specified markdown element, including and and filters by the type . - /// The descendant elements are returned in DFS-like order. - /// - /// Type to use for filtering the descendants - /// The markdown object. - /// An iteration over the descendant elements - public static IEnumerable Descendants(this MarkdownObject markdownObject) where T : MarkdownObject + /// + /// Iterates over the descendant elements for the specified markdown element, including and and filters by the type . + /// The descendant elements are returned in DFS-like order. + /// + /// Type to use for filtering the descendants + /// The markdown object. + /// An iteration over the descendant elements + public static IEnumerable Descendants(this MarkdownObject markdownObject) where T : MarkdownObject + { + if (typeof(T).IsSubclassOf(typeof(Block))) { - if (typeof(T).IsSubclassOf(typeof(Block))) + if (markdownObject is ContainerBlock containerBlock && containerBlock.Count > 0) { - if (markdownObject is ContainerBlock containerBlock && containerBlock.Count > 0) - { - return BlockDescendantsInternal(containerBlock); - } + return BlockDescendantsInternal(containerBlock); } - else // typeof(T).IsSubclassOf(typeof(Inline))) + } + else // typeof(T).IsSubclassOf(typeof(Inline))) + { + if (markdownObject is ContainerBlock containerBlock) { - if (markdownObject is ContainerBlock containerBlock) + if (containerBlock.Count > 0) { - if (containerBlock.Count > 0) - { - return InlineDescendantsInternal(containerBlock); - } - } - else if (markdownObject is ContainerInline containerInline && containerInline.FirstChild != null) - { - return containerInline.FindDescendantsInternal(); + return InlineDescendantsInternal(containerBlock); } } + else if (markdownObject is ContainerInline containerInline && containerInline.FirstChild != null) + { + return containerInline.FindDescendantsInternal(); + } + } + + return Array.Empty(); + } + + /// + /// Iterates over the descendant elements for the specified markdown element and filters by the type . + /// + /// Type to use for filtering the descendants + /// The inline markdown object. + /// + /// An iteration over the descendant elements + /// + public static IEnumerable Descendants(this ContainerInline inline) where T : Inline + => inline.FindDescendants(); + /// + /// Iterates over the descendant elements for the specified markdown element and filters by the type . + /// + /// Type to use for filtering the descendants + /// The markdown object. + /// + /// An iteration over the descendant elements + /// + public static IEnumerable Descendants(this ContainerBlock block) where T : Block + { + if (block is { Count: > 0 }) + { + return BlockDescendantsInternal(block); + } + else + { return Array.Empty(); } + } - /// - /// Iterates over the descendant elements for the specified markdown element and filters by the type . - /// - /// Type to use for filtering the descendants - /// The inline markdown object. - /// - /// An iteration over the descendant elements - /// - public static IEnumerable Descendants(this ContainerInline inline) where T : Inline - => inline.FindDescendants(); - - /// - /// Iterates over the descendant elements for the specified markdown element and filters by the type . - /// - /// Type to use for filtering the descendants - /// The markdown object. - /// - /// An iteration over the descendant elements - /// - public static IEnumerable Descendants(this ContainerBlock block) where T : Block + private static IEnumerable BlockDescendantsInternal(ContainerBlock block) where T : MarkdownObject + { + Debug.Assert(typeof(T).IsSubclassOf(typeof(Block))); + + var stack = new Stack(); + + int childrenCount = block.Count; + while (childrenCount-- > 0) { - if (block is { Count: > 0 }) - { - return BlockDescendantsInternal(block); - } - else - { - return Array.Empty(); - } + stack.Push(block[childrenCount]); } - private static IEnumerable BlockDescendantsInternal(ContainerBlock block) where T : MarkdownObject + while (stack.Count > 0) { - Debug.Assert(typeof(T).IsSubclassOf(typeof(Block))); - - var stack = new Stack(); - - int childrenCount = block.Count; - while (childrenCount-- > 0) + var subBlock = stack.Pop(); + if (subBlock is T subBlockT) { - stack.Push(block[childrenCount]); + yield return subBlockT; } - while (stack.Count > 0) + if (subBlock is ContainerBlock subBlockContainer) { - var subBlock = stack.Pop(); - if (subBlock is T subBlockT) + childrenCount = subBlockContainer.Count; + while (childrenCount-- > 0) { - yield return subBlockT; - } - - if (subBlock is ContainerBlock subBlockContainer) - { - childrenCount = subBlockContainer.Count; - while (childrenCount-- > 0) - { - stack.Push(subBlockContainer[childrenCount]); - } + stack.Push(subBlockContainer[childrenCount]); } } } + } - private static IEnumerable InlineDescendantsInternal(ContainerBlock block) where T : MarkdownObject - { - Debug.Assert(typeof(T).IsSubclassOf(typeof(Inline))); + private static IEnumerable InlineDescendantsInternal(ContainerBlock block) where T : MarkdownObject + { + Debug.Assert(typeof(T).IsSubclassOf(typeof(Inline))); - foreach (MarkdownObject descendant in block.Descendants()) + foreach (MarkdownObject descendant in block.Descendants()) + { + if (descendant is T descendantT) { - if (descendant is T descendantT) - { - yield return descendantT; - } + yield return descendantT; } } } diff --git a/src/Markdig/Syntax/NoBlocksFoundBlock.cs b/src/Markdig/Syntax/NoBlocksFoundBlock.cs index 7f0b514e5..8b21f8ed1 100644 --- a/src/Markdig/Syntax/NoBlocksFoundBlock.cs +++ b/src/Markdig/Syntax/NoBlocksFoundBlock.cs @@ -4,16 +4,15 @@ using Markdig.Parsers; -namespace Markdig.Syntax +namespace Markdig.Syntax; + +/// +/// Block representing a document with characters but no blocks. This can +/// happen when an input document consists solely of trivia. +/// +public sealed class EmptyBlock : LeafBlock { - /// - /// Block representing a document with characters but no blocks. This can - /// happen when an input document consists solely of trivia. - /// - public sealed class EmptyBlock : LeafBlock + public EmptyBlock (BlockParser? parser) : base(parser) { - public EmptyBlock (BlockParser? parser) : base(parser) - { - } } } diff --git a/src/Markdig/Syntax/ParagraphBlock.cs b/src/Markdig/Syntax/ParagraphBlock.cs index 2645dde4c..f0760305c 100644 --- a/src/Markdig/Syntax/ParagraphBlock.cs +++ b/src/Markdig/Syntax/ParagraphBlock.cs @@ -4,34 +4,33 @@ using Markdig.Parsers; -namespace Markdig.Syntax +namespace Markdig.Syntax; + +/// +/// Represents a paragraph. +/// +/// +/// Related to CommonMark spec: 4.8 Paragraphs +/// +public class ParagraphBlock : LeafBlock { /// - /// Represents a paragraph. + /// Initializes a new instance of the class. /// - /// - /// Related to CommonMark spec: 4.8 Paragraphs - /// - public class ParagraphBlock : LeafBlock + public ParagraphBlock() : this(null) { - /// - /// Initializes a new instance of the class. - /// - public ParagraphBlock() : this(null) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The parser used to create this block. - public ParagraphBlock(BlockParser? parser) : base(parser) - { - // Inlines are processed for a paragraph - ProcessInlines = true; - IsParagraphBlock = true; - } + } - public int LastLine => Line + Lines.Count - 1; + /// + /// Initializes a new instance of the class. + /// + /// The parser used to create this block. + public ParagraphBlock(BlockParser? parser) : base(parser) + { + // Inlines are processed for a paragraph + ProcessInlines = true; + IsParagraphBlock = true; } + + public int LastLine => Line + Lines.Count - 1; } \ No newline at end of file diff --git a/src/Markdig/Syntax/QuoteBlock.cs b/src/Markdig/Syntax/QuoteBlock.cs index 51116bdc2..6a1234679 100644 --- a/src/Markdig/Syntax/QuoteBlock.cs +++ b/src/Markdig/Syntax/QuoteBlock.cs @@ -4,70 +4,68 @@ using Markdig.Helpers; using Markdig.Parsers; -using System.Collections.Generic; -namespace Markdig.Syntax +namespace Markdig.Syntax; + +/// +/// A block quote (Section 5.1 CommonMark specs) +/// +/// +public class QuoteBlock : ContainerBlock { + private List Trivia => GetOrSetDerivedTrivia>(); + /// - /// A block quote (Section 5.1 CommonMark specs) + /// Initializes a new instance of the class. /// - /// - public class QuoteBlock : ContainerBlock + /// The parser used to create this block. + public QuoteBlock(BlockParser parser) : base(parser) { - private List Trivia => GetOrSetDerivedTrivia>(); - - /// - /// Initializes a new instance of the class. - /// - /// The parser used to create this block. - public QuoteBlock(BlockParser parser) : base(parser) - { - } + } - /// - /// Gets or sets the trivia per line of this QuoteBlock. - /// Trivia: only parsed when is enabled, otherwise null. - /// - public List QuoteLines => Trivia; + /// + /// Gets or sets the trivia per line of this QuoteBlock. + /// Trivia: only parsed when is enabled, otherwise null. + /// + public List QuoteLines => Trivia; - /// - /// Gets or sets the quote character (usually `>`) - /// - public char QuoteChar { get; set; } - } + /// + /// Gets or sets the quote character (usually `>`) + /// + public char QuoteChar { get; set; } +} +/// +/// Represents trivia per line part of a QuoteBlock. +/// Trivia: only parsed when is enabled. +/// +public class QuoteBlockLine +{ /// - /// Represents trivia per line part of a QuoteBlock. - /// Trivia: only parsed when is enabled. + /// Gets or sets trivia occuring before the first quote character. /// - public class QuoteBlockLine - { - /// - /// Gets or sets trivia occuring before the first quote character. - /// - public StringSlice TriviaBefore { get; set; } + public StringSlice TriviaBefore { get; set; } - /// - /// True when this QuoteBlock line has a quote character. False when - /// this line is a "lazy line". - /// - public bool QuoteChar { get; set; } + /// + /// True when this QuoteBlock line has a quote character. False when + /// this line is a "lazy line". + /// + public bool QuoteChar { get; set; } - /// - /// True if a space is parsed right after the quote character. - /// - public bool HasSpaceAfterQuoteChar { get; set; } + /// + /// True if a space is parsed right after the quote character. + /// + public bool HasSpaceAfterQuoteChar { get; set; } - /// - /// Gets or sets the trivia after the the space after the quote character. - /// The first space is assigned to , subsequent - /// trivia is assigned to this property. - /// - public StringSlice TriviaAfter { get; set; } + /// + /// Gets or sets the trivia after the the space after the quote character. + /// The first space is assigned to , subsequent + /// trivia is assigned to this property. + /// + public StringSlice TriviaAfter { get; set; } - /// - /// Gets or sets the newline of this QuoeBlockLine. - /// - public NewLine NewLine { get; set; } - } + /// + /// Gets or sets the newline of this QuoeBlockLine. + /// + public NewLine NewLine { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Syntax/SourceSpan.cs b/src/Markdig/Syntax/SourceSpan.cs index 538b59825..c63702d24 100644 --- a/src/Markdig/Syntax/SourceSpan.cs +++ b/src/Markdig/Syntax/SourceSpan.cs @@ -2,83 +2,80 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using System; +namespace Markdig.Syntax; -namespace Markdig.Syntax +/// +/// A span of text. +/// +public struct SourceSpan : IEquatable { + public static readonly SourceSpan Empty = new SourceSpan(0, -1); + /// - /// A span of text. + /// Initializes a new instance of the struct. /// - public struct SourceSpan : IEquatable + /// The start. + /// The end. + public SourceSpan(int start, int end) { - public static readonly SourceSpan Empty = new SourceSpan(0, -1); - - /// - /// Initializes a new instance of the struct. - /// - /// The start. - /// The end. - public SourceSpan(int start, int end) - { - Start = start; - End = end; - } + Start = start; + End = end; + } - /// - /// Gets or sets the starting character position from the original text source. - /// Note that for inline elements, this is only valid if is setup on the pipeline. - /// - public int Start { get; set; } + /// + /// Gets or sets the starting character position from the original text source. + /// Note that for inline elements, this is only valid if is setup on the pipeline. + /// + public int Start { get; set; } - /// - /// Gets or sets the ending character position from the original text source. - /// Note that for inline elements, this is only valid if is setup on the pipeline. - /// - public int End { get; set; } + /// + /// Gets or sets the ending character position from the original text source. + /// Note that for inline elements, this is only valid if is setup on the pipeline. + /// + public int End { get; set; } - /// - /// Gets the character length of this element within the original source code. - /// - public int Length => End - Start + 1; + /// + /// Gets the character length of this element within the original source code. + /// + public int Length => End - Start + 1; - public bool IsEmpty => Start > End; + public bool IsEmpty => Start > End; - public SourceSpan MoveForward(int count) - { - return new SourceSpan(Start + count, End + count); - } + public SourceSpan MoveForward(int count) + { + return new SourceSpan(Start + count, End + count); + } - public bool Equals(SourceSpan other) - { - return Start == other.Start && End == other.End; - } + public bool Equals(SourceSpan other) + { + return Start == other.Start && End == other.End; + } - public override bool Equals(object? obj) - { - return obj is SourceSpan sourceSpan && Equals(sourceSpan); - } + public override bool Equals(object? obj) + { + return obj is SourceSpan sourceSpan && Equals(sourceSpan); + } - public override int GetHashCode() + public override int GetHashCode() + { + unchecked { - unchecked - { - return (Start*397) ^ End; - } + return (Start*397) ^ End; } + } - public static bool operator ==(SourceSpan left, SourceSpan right) - { - return left.Equals(right); - } + public static bool operator ==(SourceSpan left, SourceSpan right) + { + return left.Equals(right); + } - public static bool operator !=(SourceSpan left, SourceSpan right) - { - return !left.Equals(right); - } + public static bool operator !=(SourceSpan left, SourceSpan right) + { + return !left.Equals(right); + } - public override string ToString() - { - return $"{Start}-{End}"; - } + public override string ToString() + { + return $"{Start}-{End}"; } } \ No newline at end of file diff --git a/src/Markdig/Syntax/ThematicBreakBlock.cs b/src/Markdig/Syntax/ThematicBreakBlock.cs index f74842553..69aa3db64 100644 --- a/src/Markdig/Syntax/ThematicBreakBlock.cs +++ b/src/Markdig/Syntax/ThematicBreakBlock.cs @@ -5,25 +5,24 @@ using Markdig.Helpers; using Markdig.Parsers; -namespace Markdig.Syntax +namespace Markdig.Syntax; + +/// +/// Represents a thematic break (Section 4.1 CommonMark specs). +/// +public class ThematicBreakBlock : LeafBlock { /// - /// Represents a thematic break (Section 4.1 CommonMark specs). + /// Initializes a new instance of the class. /// - public class ThematicBreakBlock : LeafBlock + /// The parser used to create this block. + public ThematicBreakBlock(BlockParser parser) : base(parser) { - /// - /// Initializes a new instance of the class. - /// - /// The parser used to create this block. - public ThematicBreakBlock(BlockParser parser) : base(parser) - { - } + } - public char ThematicChar { get; set; } + public char ThematicChar { get; set; } - public int ThematicCharCount { get; set; } + public int ThematicCharCount { get; set; } - public StringSlice Content; - } + public StringSlice Content; } \ No newline at end of file diff --git a/src/SpecFileGen/Program.cs b/src/SpecFileGen/Program.cs index 94c80208a..bc487249f 100644 --- a/src/SpecFileGen/Program.cs +++ b/src/SpecFileGen/Program.cs @@ -1,380 +1,376 @@ -using System; -using System.Collections.Generic; -using System.IO; using System.Text; -namespace SpecFileGen +namespace SpecFileGen; + +class Program { - class Program + static readonly string SpecificationsDirectory = + Path.GetFullPath( + Path.Combine( + Path.GetDirectoryName(typeof(Spec).Assembly.Location), + "../../../../Markdig.Tests/")); + + enum RendererType { - static readonly string SpecificationsDirectory = - Path.GetFullPath( - Path.Combine( - Path.GetDirectoryName(typeof(Spec).Assembly.Location), - "../../../../Markdig.Tests/")); + Html, + Normalize, + PlainText, + Roundtrip + } - enum RendererType + class Spec + { + public readonly string Name; + public readonly string Path; + public readonly string OutputPath; + public readonly string Extensions; + public readonly RendererType RendererType; + public int TestCount; + + public Spec(string name, string fileName, string extensions, RendererType rendererType = RendererType.Html) { - Html, - Normalize, - PlainText, - Roundtrip + Name = name; + Path = SpecificationsDirectory; + if (rendererType == RendererType.Html) Path += "Specs"; + else if (rendererType == RendererType.Normalize) Path += "NormalizeSpecs"; + else if (rendererType == RendererType.PlainText) Path += "PlainTextSpecs"; + else if (rendererType == RendererType.Roundtrip) Path += "RoundtripSpecs"; + Path += "/" + fileName; + OutputPath = System.IO.Path.ChangeExtension(Path, "generated.cs"); + Extensions = extensions; + RendererType = rendererType; } + } + class NormalizeSpec : Spec + { + public NormalizeSpec(string name, string fileName, string extensions) + : base(name, fileName, extensions, rendererType: RendererType.Normalize) { } + } + class PlainTextSpec : Spec + { + public PlainTextSpec(string name, string fileName, string extensions) + : base(name, fileName, extensions, rendererType: RendererType.PlainText) { } + } + class RoundtripSpec : Spec + { + public RoundtripSpec(string name, string fileName, string extensions) + : base(name, fileName, extensions, rendererType: RendererType.Roundtrip) { } + } + + // NOTE: Beware of Copy/Pasting spec files - some characters may change (non-breaking space into space)! + static readonly Spec[] Specs = new[] + { + new Spec("CommonMarkSpecs", "CommonMark.md", ""), + new Spec("Pipe Tables", "PipeTableSpecs.md", "pipetables|advanced"), + new Spec("GFM Pipe Tables", "PipeTableGfmSpecs.md", "gfm-pipetables"), + new Spec("Footnotes", "FootnotesSpecs.md", "footnotes|advanced"), + new Spec("Generic Attributes", "GenericAttributesSpecs.md", "attributes|advanced"), + new Spec("Emphasis Extra", "EmphasisExtraSpecs.md", "emphasisextras|advanced"), + new Spec("Hardline Breaks", "HardlineBreakSpecs.md", "hardlinebreak|advanced+hardlinebreak"), + new Spec("Grid Tables", "GridTableSpecs.md", "gridtables|advanced"), + new Spec("Custom Containers", "CustomContainerSpecs.md", "customcontainers+attributes|advanced"), + new Spec("Definition Lists", "DefinitionListSpecs.md", "definitionlists+attributes|advanced"), + new Spec("Emoji", "EmojiSpecs.md", "emojis|advanced+emojis"), + new Spec("Abbreviations", "AbbreviationSpecs.md", "abbreviations|advanced"), + new Spec("List Extras", "ListExtraSpecs.md", "listextras|advanced"), + new Spec("Math", "MathSpecs.md", "mathematics|advanced"), + new Spec("Bootstrap", "BootstrapSpecs.md", "bootstrap+pipetables+figures+attributes"), + new Spec("Media", "MediaSpecs.md", "medialinks|advanced+medialinks"), + new Spec("Smarty Pants", "SmartyPantsSpecs.md", "pipetables+smartypants|advanced+smartypants"), + new Spec("Auto Identifiers", "AutoIdentifierSpecs.md", "autoidentifiers|advanced"), + new Spec("Task Lists", "TaskListSpecs.md", "tasklists|advanced"), + new Spec("Diagrams", "DiagramsSpecs.md", "diagrams|advanced"), + new Spec("No Html", "NoHtmlSpecs.md", "nohtml"), + new Spec("Yaml", "YamlSpecs.md", "yaml"), + new Spec("Auto Links", "AutoLinks.md", "autolinks|advanced"), + new Spec("Jira Links", "JiraLinks.md", "jiralinks"), + new Spec("Globalization", "GlobalizationSpecs.md", "globalization+advanced+emojis"), + new Spec("Figures, Footers and Cites", "FigureFooterAndCiteSpecs.md", "figures+footers+citations|advanced"), + + new NormalizeSpec("Headings", "Headings.md", ""), + + new PlainTextSpec("Sample", "SamplePlainText.md", ""), + + new RoundtripSpec("Roundtrip", "CommonMark.md", ""), + }; + + static void Main() + { + int totalTests = 0; + bool hasErrors = false; - class Spec + foreach (var spec in Specs) { - public readonly string Name; - public readonly string Path; - public readonly string OutputPath; - public readonly string Extensions; - public readonly RendererType RendererType; - public int TestCount; - - public Spec(string name, string fileName, string extensions, RendererType rendererType = RendererType.Html) + if (!File.Exists(spec.Path)) { - Name = name; - Path = SpecificationsDirectory; - if (rendererType == RendererType.Html) Path += "Specs"; - else if (rendererType == RendererType.Normalize) Path += "NormalizeSpecs"; - else if (rendererType == RendererType.PlainText) Path += "PlainTextSpecs"; - else if (rendererType == RendererType.Roundtrip) Path += "RoundtripSpecs"; - Path += "/" + fileName; - OutputPath = System.IO.Path.ChangeExtension(Path, "generated.cs"); - Extensions = extensions; - RendererType = rendererType; + EmitError("Could not find the specification file at " + spec.Path); + hasErrors = true; + continue; } - } - class NormalizeSpec : Spec - { - public NormalizeSpec(string name, string fileName, string extensions) - : base(name, fileName, extensions, rendererType: RendererType.Normalize) { } - } - class PlainTextSpec : Spec - { - public PlainTextSpec(string name, string fileName, string extensions) - : base(name, fileName, extensions, rendererType: RendererType.PlainText) { } - } - class RoundtripSpec : Spec - { - public RoundtripSpec(string name, string fileName, string extensions) - : base(name, fileName, extensions, rendererType: RendererType.Roundtrip) { } - } - // NOTE: Beware of Copy/Pasting spec files - some characters may change (non-breaking space into space)! - static readonly Spec[] Specs = new[] - { - new Spec("CommonMarkSpecs", "CommonMark.md", ""), - new Spec("Pipe Tables", "PipeTableSpecs.md", "pipetables|advanced"), - new Spec("GFM Pipe Tables", "PipeTableGfmSpecs.md", "gfm-pipetables"), - new Spec("Footnotes", "FootnotesSpecs.md", "footnotes|advanced"), - new Spec("Generic Attributes", "GenericAttributesSpecs.md", "attributes|advanced"), - new Spec("Emphasis Extra", "EmphasisExtraSpecs.md", "emphasisextras|advanced"), - new Spec("Hardline Breaks", "HardlineBreakSpecs.md", "hardlinebreak|advanced+hardlinebreak"), - new Spec("Grid Tables", "GridTableSpecs.md", "gridtables|advanced"), - new Spec("Custom Containers", "CustomContainerSpecs.md", "customcontainers+attributes|advanced"), - new Spec("Definition Lists", "DefinitionListSpecs.md", "definitionlists+attributes|advanced"), - new Spec("Emoji", "EmojiSpecs.md", "emojis|advanced+emojis"), - new Spec("Abbreviations", "AbbreviationSpecs.md", "abbreviations|advanced"), - new Spec("List Extras", "ListExtraSpecs.md", "listextras|advanced"), - new Spec("Math", "MathSpecs.md", "mathematics|advanced"), - new Spec("Bootstrap", "BootstrapSpecs.md", "bootstrap+pipetables+figures+attributes"), - new Spec("Media", "MediaSpecs.md", "medialinks|advanced+medialinks"), - new Spec("Smarty Pants", "SmartyPantsSpecs.md", "pipetables+smartypants|advanced+smartypants"), - new Spec("Auto Identifiers", "AutoIdentifierSpecs.md", "autoidentifiers|advanced"), - new Spec("Task Lists", "TaskListSpecs.md", "tasklists|advanced"), - new Spec("Diagrams", "DiagramsSpecs.md", "diagrams|advanced"), - new Spec("No Html", "NoHtmlSpecs.md", "nohtml"), - new Spec("Yaml", "YamlSpecs.md", "yaml"), - new Spec("Auto Links", "AutoLinks.md", "autolinks|advanced"), - new Spec("Jira Links", "JiraLinks.md", "jiralinks"), - new Spec("Globalization", "GlobalizationSpecs.md", "globalization+advanced+emojis"), - new Spec("Figures, Footers and Cites", "FigureFooterAndCiteSpecs.md", "figures+footers+citations|advanced"), - - new NormalizeSpec("Headings", "Headings.md", ""), - - new PlainTextSpec("Sample", "SamplePlainText.md", ""), - - new RoundtripSpec("Roundtrip", "CommonMark.md", ""), - }; - - static void Main() - { - int totalTests = 0; - bool hasErrors = false; + string source = ParseSpecification(spec, File.ReadAllText(spec.Path)).Replace("\r\n", "\n", StringComparison.Ordinal); + totalTests += spec.TestCount; - foreach (var spec in Specs) + if (File.Exists(spec.OutputPath)) // If the source hasn't changed, don't bump the generated tag { - if (!File.Exists(spec.Path)) + string previousSource = File.ReadAllText(spec.OutputPath).Replace("\r\n", "\n", StringComparison.Ordinal); + if (previousSource == source && File.GetLastWriteTime(spec.OutputPath) > File.GetLastWriteTime(spec.Path)) { - EmitError("Could not find the specification file at " + spec.Path); - hasErrors = true; continue; } - - string source = ParseSpecification(spec, File.ReadAllText(spec.Path)).Replace("\r\n", "\n", StringComparison.Ordinal); - totalTests += spec.TestCount; - - if (File.Exists(spec.OutputPath)) // If the source hasn't changed, don't bump the generated tag - { - string previousSource = File.ReadAllText(spec.OutputPath).Replace("\r\n", "\n", StringComparison.Ordinal); - if (previousSource == source && File.GetLastWriteTime(spec.OutputPath) > File.GetLastWriteTime(spec.Path)) - { - continue; - } - Console.WriteLine($"Spec changed {spec.Path}. Need to regenerate"); - } - Console.WriteLine($"Generating spec {spec.Name} to {spec.OutputPath}..."); - File.WriteAllText(spec.OutputPath, source); + Console.WriteLine($"Spec changed {spec.Path}. Need to regenerate"); } - - if (hasErrors) - { - Environment.Exit(1); - } - - Console.WriteLine("There are {0} spec tests in total", totalTests); - } - static void EmitError(string error) - { - var prevColor = Console.ForegroundColor; - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine(); - Console.WriteLine(new string('-', error.Length)); - Console.WriteLine(error); - Console.WriteLine(new string('-', error.Length)); - Console.WriteLine(); - Console.ForegroundColor = prevColor; + Console.WriteLine($"Generating spec {spec.Name} to {spec.OutputPath}..."); + File.WriteAllText(spec.OutputPath, source); } - static readonly StringBuilder StringBuilder = new StringBuilder(1 << 20); // 1 MB - static void Write(string text) - { - StringBuilder.Append(text); - } - static void Line(string text = null) - { - if (text != null) StringBuilder.Append(text); - StringBuilder.Append('\n'); - } - static void Indent(int count = 1) + if (hasErrors) { - StringBuilder.Append(new string(' ', 4 * count)); + Environment.Exit(1); } - static string ParseSpecification(Spec spec, string specSource) + Console.WriteLine("There are {0} spec tests in total", totalTests); + } + static void EmitError(string error) + { + var prevColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(); + Console.WriteLine(new string('-', error.Length)); + Console.WriteLine(error); + Console.WriteLine(new string('-', error.Length)); + Console.WriteLine(); + Console.ForegroundColor = prevColor; + } + + static readonly StringBuilder StringBuilder = new StringBuilder(1 << 20); // 1 MB + static void Write(string text) + { + StringBuilder.Append(text); + } + static void Line(string text = null) + { + if (text != null) StringBuilder.Append(text); + StringBuilder.Append('\n'); + } + static void Indent(int count = 1) + { + StringBuilder.Append(new string(' ', 4 * count)); + } + + static string ParseSpecification(Spec spec, string specSource) + { + Line(); + Write("// "); Line(new string('-', 32)); + Write("// "); Write(new string(' ', 16 - spec.Name.Length / 2)); Line(spec.Name); + Write("// "); Line(new string('-', 32)); + Line(); + Line("using System;"); + Line("using NUnit.Framework;"); + Line(); + Write("namespace Markdig.Tests.Specs."); + if (spec.RendererType == RendererType.Normalize) Write("Normalize."); + else if (spec.RendererType == RendererType.PlainText) Write("PlainText."); + else if (spec.RendererType == RendererType.Roundtrip) Write("Roundtrip."); + Line(CompressedName(spec.Name).Replace('.', '_')); + Line("{"); + + var lines = specSource.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.None); + + bool nameChanged = true; + string name = ""; + string compressedName = ""; + int number = 0; + int commentOffset = 0, commentEnd = 0, markdownOffset = 0, markdownEnd = 0, htmlOffset = 0, htmlEnd = 0; + bool first = true; + LinkedList<(string Heading, string Compressed, int Level)> headings = new LinkedList<(string, string, int)>(); + StringBuilder nameBuilder = new StringBuilder(64); + + int i = 0; + while (i < lines.Length) { - Line(); - Write("// "); Line(new string('-', 32)); - Write("// "); Write(new string(' ', 16 - spec.Name.Length / 2)); Line(spec.Name); - Write("// "); Line(new string('-', 32)); - Line(); - Line("using System;"); - Line("using NUnit.Framework;"); - Line(); - Write("namespace Markdig.Tests.Specs."); - if (spec.RendererType == RendererType.Normalize) Write("Normalize."); - else if (spec.RendererType == RendererType.PlainText) Write("PlainText."); - else if (spec.RendererType == RendererType.Roundtrip) Write("Roundtrip."); - Line(CompressedName(spec.Name).Replace('.', '_')); - Line("{"); - - var lines = specSource.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.None); - - bool nameChanged = true; - string name = ""; - string compressedName = ""; - int number = 0; - int commentOffset = 0, commentEnd = 0, markdownOffset = 0, markdownEnd = 0, htmlOffset = 0, htmlEnd = 0; - bool first = true; - LinkedList<(string Heading, string Compressed, int Level)> headings = new LinkedList<(string, string, int)>(); - StringBuilder nameBuilder = new StringBuilder(64); - - int i = 0; - while (i < lines.Length) + commentOffset = commentEnd = i; + while (!lines[i].Equals("```````````````````````````````` example", StringComparison.Ordinal)) { - commentOffset = commentEnd = i; - while (!lines[i].Equals("```````````````````````````````` example", StringComparison.Ordinal)) + string line = lines[i]; + if (line.Length > 2 && line[0] == '#') { - string line = lines[i]; - if (line.Length > 2 && line[0] == '#') + int level = line.IndexOf(' ', StringComparison.Ordinal); + while (headings.Count != 0) { - int level = line.IndexOf(' ', StringComparison.Ordinal); - while (headings.Count != 0) - { - if (headings.Last.Value.Level < level) break; - headings.RemoveLast(); - } - string heading = line.Substring(level + 1); - headings.AddLast((heading, CompressedName(heading), level)); + if (headings.Last.Value.Level < level) break; + headings.RemoveLast(); + } + string heading = line.Substring(level + 1); + headings.AddLast((heading, CompressedName(heading), level)); - foreach (var (Heading, _, _) in headings) - nameBuilder.Append(Heading + " / "); - nameBuilder.Length -= 3; - name = nameBuilder.ToString(); - nameBuilder.Length = 0; + foreach (var (Heading, _, _) in headings) + nameBuilder.Append(Heading + " / "); + nameBuilder.Length -= 3; + name = nameBuilder.ToString(); + nameBuilder.Length = 0; - foreach (var (_, Compressed, _) in headings) - nameBuilder.Append(Compressed); - compressedName = nameBuilder.ToString(); - nameBuilder.Length = 0; + foreach (var (_, Compressed, _) in headings) + nameBuilder.Append(Compressed); + compressedName = nameBuilder.ToString(); + nameBuilder.Length = 0; - nameChanged = true; - } - i++; + nameChanged = true; + } + i++; - if (!IsEmpty(line)) - commentEnd = i; + if (!IsEmpty(line)) + commentEnd = i; - if (i == lines.Length) + if (i == lines.Length) + { + if (commentOffset != commentEnd) { - if (commentOffset != commentEnd) + while (commentOffset < commentEnd && IsEmpty(lines[commentOffset])) commentOffset++; + for (i = commentOffset; i < commentEnd; i++) { - while (commentOffset < commentEnd && IsEmpty(lines[commentOffset])) commentOffset++; - for (i = commentOffset; i < commentEnd; i++) - { - Indent(2); Write("// "); Line(lines[i]); - } + Indent(2); Write("// "); Line(lines[i]); } - goto End; } - }; + goto End; + } + }; - markdownOffset = ++i; - while (!(lines[i].Length == 1 && lines[i][0] == '.')) i++; - markdownEnd = i++; + markdownOffset = ++i; + while (!(lines[i].Length == 1 && lines[i][0] == '.')) i++; + markdownEnd = i++; - htmlOffset = i; - while (!lines[i].Equals("````````````````````````````````", StringComparison.Ordinal)) i++; - htmlEnd = i++; + htmlOffset = i; + while (!lines[i].Equals("````````````````````````````````", StringComparison.Ordinal)) i++; + htmlEnd = i++; - if (nameChanged) + if (nameChanged) + { + if (!first) { - if (!first) - { - Indent(); Line("}"); - Line(); - } - Indent(); Line("[TestFixture]"); - Indent(); Line("public class Test" + compressedName); - Indent(); Line("{"); - first = false; - nameChanged = false; + Indent(); Line("}"); + Line(); } - else Line(); - - WriteTest(name, compressedName, ++number, spec.Extensions, lines, - commentOffset, commentEnd, - markdownOffset, markdownEnd, - htmlOffset, htmlEnd, - spec.RendererType); - } - - End: - if (!first) - { - Indent(); Line("}"); + Indent(); Line("[TestFixture]"); + Indent(); Line("public class Test" + compressedName); + Indent(); Line("{"); + first = false; + nameChanged = false; } - Line("}"); + else Line(); - string source = StringBuilder.ToString(); - StringBuilder.Length = 0; - - spec.TestCount = number; - return source; + WriteTest(name, compressedName, ++number, spec.Extensions, lines, + commentOffset, commentEnd, + markdownOffset, markdownEnd, + htmlOffset, htmlEnd, + spec.RendererType); } - static void WriteTest(string name, string compressedName, int number, string extensions, string[] lines, int commentOffset, int commentEnd, int markdownOffset, int markdownEnd, int htmlOffset, int htmlEnd, RendererType rendererType) + End: + if (!first) { - if (commentOffset != commentEnd) - { - while (commentOffset < commentEnd && IsEmpty(lines[commentOffset])) commentOffset++; - for (int i = commentOffset; i < commentEnd; i++) - { - Indent(2); Write("// "); Line(lines[i]); - } - } + Indent(); Line("}"); + } + Line("}"); - Indent(2); Line("[Test]"); - Indent(2); Line("public void " + compressedName + "_Example" + number.ToString().PadLeft(3, '0') + "()"); - Indent(2); Line("{"); - Indent(3); Line("// Example " + number); - Indent(3); Line("// Section: " + name); + string source = StringBuilder.ToString(); + StringBuilder.Length = 0; - Indent(3); Line("//"); - Indent(3); Line("// The following Markdown:"); - for (int i = markdownOffset; i < markdownEnd; i++) - { - Indent(3); Write("// "); Indent(); Line(lines[i]); - } + spec.TestCount = number; + return source; + } - Indent(3); Line("//"); - Indent(3); Line("// Should be rendered as:"); - for (int i = htmlOffset; i < htmlEnd; i++) - { - Indent(3); Write("// "); Indent(); Line(lines[i]); - } - if (htmlOffset >= htmlEnd) - { - Indent(3); Write("//"); - } - Line(); - Indent(3); - if (rendererType == RendererType.Html) Write("TestParser"); - else if (rendererType == RendererType.Normalize) Write("TestNormalize"); - else if (rendererType == RendererType.PlainText) Write("TestPlainText"); - else if (rendererType == RendererType.Roundtrip) Write("TestRoundtrip"); - Write(".TestSpec(\""); - for (int i = markdownOffset; i < markdownEnd; i++) - { - Write(Escape(lines[i])); - if (i != markdownEnd - 1) Write("\\n"); - } - Write("\", \""); - for (int i = htmlOffset; i < htmlEnd; i++) + static void WriteTest(string name, string compressedName, int number, string extensions, string[] lines, int commentOffset, int commentEnd, int markdownOffset, int markdownEnd, int htmlOffset, int htmlEnd, RendererType rendererType) + { + if (commentOffset != commentEnd) + { + while (commentOffset < commentEnd && IsEmpty(lines[commentOffset])) commentOffset++; + for (int i = commentOffset; i < commentEnd; i++) { - Write(Escape(lines[i])); - if (i != htmlEnd - 1) Write("\\n"); + Indent(2); Write("// "); Line(lines[i]); } - Write("\", \""); - Write(extensions); - Line($"\", context: \"Example {number}\\nSection {name}\\n\");"); + } + + Indent(2); Line("[Test]"); + Indent(2); Line("public void " + compressedName + "_Example" + number.ToString().PadLeft(3, '0') + "()"); + Indent(2); Line("{"); + Indent(3); Line("// Example " + number); + Indent(3); Line("// Section: " + name); - Indent(2); Line("}"); + Indent(3); Line("//"); + Indent(3); Line("// The following Markdown:"); + for (int i = markdownOffset; i < markdownEnd; i++) + { + Indent(3); Write("// "); Indent(); Line(lines[i]); } - static string Escape(string input) + + Indent(3); Line("//"); + Indent(3); Line("// Should be rendered as:"); + for (int i = htmlOffset; i < htmlEnd; i++) { - return input - .Replace("→", "\t") - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\0", "\\0") - .Replace("\a", "\\a") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - .Replace("\v", "\\v") - ; + Indent(3); Write("// "); Indent(); Line(lines[i]); } - static string CompressedName(string name) + if (htmlOffset >= htmlEnd) { - string compressedName = ""; - foreach (var part in name.Replace(',', ' ').Split(' ', StringSplitOptions.RemoveEmptyEntries)) - { - compressedName += char.IsLower(part[0]) - ? char.ToUpper(part[0]) + (part.Length > 1 ? part.Substring(1) : "") - : part; - } - return compressedName; + Indent(3); Write("//"); } - static bool IsEmpty(string str) + Line(); + Indent(3); + if (rendererType == RendererType.Html) Write("TestParser"); + else if (rendererType == RendererType.Normalize) Write("TestNormalize"); + else if (rendererType == RendererType.PlainText) Write("TestPlainText"); + else if (rendererType == RendererType.Roundtrip) Write("TestRoundtrip"); + Write(".TestSpec(\""); + for (int i = markdownOffset; i < markdownEnd; i++) { - for (int i = 0; i < str.Length; i++) - { - if (str[i] != ' ') return false; - } - return true; + Write(Escape(lines[i])); + if (i != markdownEnd - 1) Write("\\n"); + } + Write("\", \""); + for (int i = htmlOffset; i < htmlEnd; i++) + { + Write(Escape(lines[i])); + if (i != htmlEnd - 1) Write("\\n"); + } + Write("\", \""); + Write(extensions); + Line($"\", context: \"Example {number}\\nSection {name}\\n\");"); + + Indent(2); Line("}"); + } + static string Escape(string input) + { + return input + .Replace("→", "\t") + .Replace("\\", "\\\\") + .Replace("\"", "\\\"") + .Replace("\0", "\\0") + .Replace("\a", "\\a") + .Replace("\b", "\\b") + .Replace("\f", "\\f") + .Replace("\n", "\\n") + .Replace("\r", "\\r") + .Replace("\t", "\\t") + .Replace("\v", "\\v") + ; + } + static string CompressedName(string name) + { + string compressedName = ""; + foreach (var part in name.Replace(',', ' ').Split(' ', StringSplitOptions.RemoveEmptyEntries)) + { + compressedName += char.IsLower(part[0]) + ? char.ToUpper(part[0]) + (part.Length > 1 ? part.Substring(1) : "") + : part; + } + return compressedName; + } + static bool IsEmpty(string str) + { + for (int i = 0; i < str.Length; i++) + { + if (str[i] != ' ') return false; } + return true; } } diff --git a/src/SpecFileGen/SpecFileGen.csproj b/src/SpecFileGen/SpecFileGen.csproj index 369049984..58bc33bfb 100644 --- a/src/SpecFileGen/SpecFileGen.csproj +++ b/src/SpecFileGen/SpecFileGen.csproj @@ -3,6 +3,7 @@ Exe net6.0 + enable false diff --git a/src/UnicodeNormDApp/Program.cs b/src/UnicodeNormDApp/Program.cs index b86794276..9c96b4193 100644 --- a/src/UnicodeNormDApp/Program.cs +++ b/src/UnicodeNormDApp/Program.cs @@ -1,102 +1,96 @@ -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.IO; -using System.Net.Http; using System.Text; -using System.Threading.Tasks; -namespace UnicodeNormDApp +namespace UnicodeNormDApp; + +class Program { - class Program + static async Task Main(string[] args) { - static async Task Main(string[] args) - { - using var httpClient = new HttpClient(); - var data = await httpClient.GetStringAsync("http://www.unicode.org/Public/UCD/latest/ucd/NormalizationTest.txt"); + using var httpClient = new HttpClient(); + var data = await httpClient.GetStringAsync("http://www.unicode.org/Public/UCD/latest/ucd/NormalizationTest.txt"); - var stringReader = new StringReader(data); + var stringReader = new StringReader(data); - var sep = new char[] {';'}; - var spaceSpec = new char[] {' '}; - string line; - int count = 0; - int min = int.MaxValue; - int max = int.MinValue; - var values = new Dictionary(); - var builder = new StringBuilder(); - while ((line = stringReader.ReadLine()) != null) + var sep = new char[] {';'}; + var spaceSpec = new char[] {' '}; + string line; + int count = 0; + int min = int.MaxValue; + int max = int.MinValue; + var values = new Dictionary(); + var builder = new StringBuilder(); + while ((line = stringReader.ReadLine()) != null) + { + if (line.StartsWith("#") || line.StartsWith("@")) { - if (line.StartsWith("#") || line.StartsWith("@")) - { - continue; - } - var commentIndex = line.IndexOf('#'); - var dataLine = commentIndex > 0 ? line.Substring(0, commentIndex) : line; + continue; + } + var commentIndex = line.IndexOf('#'); + var dataLine = commentIndex > 0 ? line.Substring(0, commentIndex) : line; - var columns = dataLine.Split(sep, StringSplitOptions.RemoveEmptyEntries); - if (columns.Length < 4) - { - continue; - } + var columns = dataLine.Split(sep, StringSplitOptions.RemoveEmptyEntries); + if (columns.Length < 4) + { + continue; + } - // Skip multi code point - if (columns[0].IndexOf(' ') > 0) - { - continue; - } + // Skip multi code point + if (columns[0].IndexOf(' ') > 0) + { + continue; + } - var source = Convert.ToInt32(columns[0], 16); - if (source < min) - { - min = source; - } - if (source > max) - { - max = source; - } + var source = Convert.ToInt32(columns[0], 16); + if (source < min) + { + min = source; + } + if (source > max) + { + max = source; + } - var column4Space = columns[4].Split(spaceSpec, StringSplitOptions.RemoveEmptyEntries); - builder.Clear(); - for (int i = 0; i < column4Space.Length; i++) - { - var nfdFirst = Convert.ToInt32(column4Space[i], 16); - // We support only single char codepoints - string unicodeString = char.ConvertFromUtf32(nfdFirst); - // We restrict to ascii only - if (unicodeString.Length == 1 && nfdFirst > 32 && nfdFirst < 127) - { - builder.Append(unicodeString[0]); - } - } - var str = builder.ToString(); - var sourceString = char.ConvertFromUtf32(source); - // We don't keep spaces - if (sourceString.Length == 1 && str.Length > 0 && !values.ContainsKey(sourceString[0])) + var column4Space = columns[4].Split(spaceSpec, StringSplitOptions.RemoveEmptyEntries); + builder.Clear(); + for (int i = 0; i < column4Space.Length; i++) + { + var nfdFirst = Convert.ToInt32(column4Space[i], 16); + // We support only single char codepoints + string unicodeString = char.ConvertFromUtf32(nfdFirst); + // We restrict to ascii only + if (unicodeString.Length == 1 && nfdFirst > 32 && nfdFirst < 127) { - //Trace.WriteLine(columns[0] + "/" + source + ": " + char.ConvertFromUtf32(source) + " => " + (char)nfdFirst); - count++; - values.Add(sourceString[0], str); + builder.Append(unicodeString[0]); } } - - //var newValues = new Dictionary(values.Count) - //{ - // {15, 'a'} - //} - Trace.WriteLine($"CodeToAscii = new Dictionary({values.Count})"); - Trace.WriteLine("{"); - foreach (var pair in values) + var str = builder.ToString(); + var sourceString = char.ConvertFromUtf32(source); + // We don't keep spaces + if (sourceString.Length == 1 && str.Length > 0 && !values.ContainsKey(sourceString[0])) { - var escape = pair.Value.Replace("\\", @"\\").Replace("\"", "\\\""); - Trace.WriteLine($" {{'{pair.Key}',\"{escape}\"}},"); + //Trace.WriteLine(columns[0] + "/" + source + ": " + char.ConvertFromUtf32(source) + " => " + (char)nfdFirst); + count++; + values.Add(sourceString[0], str); } - Trace.WriteLine("};"); + } - //Trace.WriteLine("count: " + count); - //Trace.WriteLine("max: " + max); - //Trace.WriteLine("min: " + min); + //var newValues = new Dictionary(values.Count) + //{ + // {15, 'a'} + //} + Trace.WriteLine($"CodeToAscii = new Dictionary({values.Count})"); + Trace.WriteLine("{"); + foreach (var pair in values) + { + var escape = pair.Value.Replace("\\", @"\\").Replace("\"", "\\\""); + Trace.WriteLine($" {{'{pair.Key}',\"{escape}\"}},"); } + Trace.WriteLine("};"); + //Trace.WriteLine("count: " + count); + //Trace.WriteLine("max: " + max); + //Trace.WriteLine("min: " + min); } + } diff --git a/src/UnicodeNormDApp/UnicodeNormDApp.csproj b/src/UnicodeNormDApp/UnicodeNormDApp.csproj index 7efc5581c..eba97ab77 100644 --- a/src/UnicodeNormDApp/UnicodeNormDApp.csproj +++ b/src/UnicodeNormDApp/UnicodeNormDApp.csproj @@ -3,5 +3,6 @@ Exe net6.0 false + enable \ No newline at end of file diff --git a/src/mdtoc/Program.cs b/src/mdtoc/Program.cs index d50dc1a47..e53f6bd9f 100644 --- a/src/mdtoc/Program.cs +++ b/src/mdtoc/Program.cs @@ -1,100 +1,94 @@ // 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.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; using Markdig; using Markdig.Extensions.AutoIdentifiers; using Markdig.Renderers; using Markdig.Renderers.Html; using Markdig.Syntax; -namespace mdtoc +namespace mdtoc; + +/// +/// A tool to generate a markdown TOC from a markdown local file or a github link to a markdown file. +/// +class Program { - /// - /// A tool to generate a markdown TOC from a markdown local file or a github link to a markdown file. - /// - class Program + static void Error(string message) + { + Console.WriteLine(message); + Environment.Exit(1); + } + + static void Main(string[] args) { - static void Error(string message) + if (args.Length != 1 || args[0] is "--help" or "-help" or "/?" or "/help") { - Console.WriteLine(message); - Environment.Exit(1); + Error("Usage: mdtoc [markdown file path | http github URL]"); + return; } - static void Main(string[] args) + var path = args[0]; + string? markdown = null; + if (path.StartsWith("https:")) { - if (args.Length != 1 || args[0] == "--help" || args[0] == "-help" || args[0] == "/?" || args[0] == "/help") + if (!Uri.TryCreate(path, UriKind.Absolute, out Uri? uri)) { - Error("Usage: mdtoc [markdown file path | http github URL]"); + Error($"Unable to parse Uri `{path}`"); return; } - - var path = args[0]; - string markdown = null; - if (path.StartsWith("https:")) + // Special handling of github URL to access the raw content instead + if (uri.Host == "github.com") { - if (!Uri.TryCreate(path, UriKind.Absolute, out Uri uri)) + // https://github.com/lunet-io/scriban/blob/master/doc/language.md + // https://raw.githubusercontent.com/lunet-io/scriban/master/doc/language.md + var newPath = uri.AbsolutePath; + var paths = new List(newPath.Split(new char[] {'/'}, StringSplitOptions.RemoveEmptyEntries)); + if (paths.Count < 5 || paths[2] != "blob") { - Error($"Unable to parse Uri `{path}`"); + Error($"Invalid github.com URL `{path}`"); return; } - // Special handling of github URL to access the raw content instead - if (uri.Host == "github.com") - { - // https://github.com/lunet-io/scriban/blob/master/doc/language.md - // https://raw.githubusercontent.com/lunet-io/scriban/master/doc/language.md - var newPath = uri.AbsolutePath; - var paths = new List(newPath.Split(new char[] {'/'}, StringSplitOptions.RemoveEmptyEntries)); - if (paths.Count < 5 || paths[2] != "blob") - { - Error($"Invalid github.com URL `{path}`"); - return; - } - paths.RemoveAt(2); // remove blob - uri = new Uri($"https://raw.githubusercontent.com/{(string.Join("/", paths))}"); - } - - var httpClient = new HttpClient(); - markdown = httpClient.GetStringAsync(uri).ConfigureAwait(false).GetAwaiter().GetResult(); - } - else - { - markdown = File.ReadAllText(path); + paths.RemoveAt(2); // remove blob + uri = new Uri($"https://raw.githubusercontent.com/{(string.Join("/", paths))}"); } - var pipeline = new MarkdownPipelineBuilder().UseAutoIdentifiers(AutoIdentifierOptions.GitHub).Build(); - var doc = Markdown.Parse(markdown, pipeline); + var httpClient = new HttpClient(); + markdown = httpClient.GetStringAsync(uri).ConfigureAwait(false).GetAwaiter().GetResult(); + } + else + { + markdown = File.ReadAllText(path); + } - // Precomputes the minHeading - var headings = doc.Descendants().ToList(); - int minHeading = int.MaxValue; - int maxHeading = int.MinValue; - foreach (var heading in headings) - { - minHeading = Math.Min(minHeading, heading.Level); - maxHeading = Math.Max(maxHeading, heading.Level); - } + var pipeline = new MarkdownPipelineBuilder().UseAutoIdentifiers(AutoIdentifierOptions.GitHub).Build(); + var doc = Markdown.Parse(markdown, pipeline); + + // Precomputes the minHeading + var headings = doc.Descendants().ToList(); + int minHeading = int.MaxValue; + int maxHeading = int.MinValue; + foreach (var heading in headings) + { + minHeading = Math.Min(minHeading, heading.Level); + maxHeading = Math.Max(maxHeading, heading.Level); + } - var writer = Console.Out; - // Use this htmlWriter to write content of headings into link label - var htmlWriter = new HtmlRenderer(writer) {EnableHtmlForInline = true}; - foreach (var heading in headings) + var writer = Console.Out; + // Use this htmlWriter to write content of headings into link label + var htmlWriter = new HtmlRenderer(writer) {EnableHtmlForInline = true}; + foreach (var heading in headings) + { + var indent = heading.Level - minHeading; + for (int i = 0; i < indent; i++) { - var indent = heading.Level - minHeading; - for (int i = 0; i < indent; i++) - { - // - Start Of Heading - writer.Write(" "); - } - writer.Write("- ["); - htmlWriter.WriteLeafInline(heading); - writer.Write($"](#{heading.GetAttributes().Id})"); - writer.WriteLine(); + // - Start Of Heading + writer.Write(" "); } + writer.Write("- ["); + htmlWriter.WriteLeafInline(heading); + writer.Write($"](#{heading.GetAttributes().Id})"); + writer.WriteLine(); } } } diff --git a/src/mdtoc/mdtoc.csproj b/src/mdtoc/mdtoc.csproj index 756bb026e..e0c014c5d 100644 --- a/src/mdtoc/mdtoc.csproj +++ b/src/mdtoc/mdtoc.csproj @@ -3,6 +3,8 @@ Exe net6.0 false + enable + enable