diff --git a/Directory.Packages.props b/Directory.Packages.props index 3bd3a81e..e92595ad 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -59,8 +59,8 @@ - - + + diff --git a/benchmarks/Benchmarks.InfiniBlazor.Markdown/IndividualMarkdownBenchmarks.cs b/benchmarks/Benchmarks.InfiniBlazor.Markdown/IndividualMarkdownBenchmarks.cs index b82d87ec..bc75bfb5 100644 --- a/benchmarks/Benchmarks.InfiniBlazor.Markdown/IndividualMarkdownBenchmarks.cs +++ b/benchmarks/Benchmarks.InfiniBlazor.Markdown/IndividualMarkdownBenchmarks.cs @@ -5,7 +5,10 @@ using BenchmarkDotNet.Order; using InfiniBlazor.Markdown; using InfiniBlazor.Markdown.Syntax; +using Microsoft.AspNetCore.Components; using Microsoft.Extensions.DependencyInjection; +using Microsoft.JSInterop; +using System.Text; namespace Benchmarks.InfiniBlazor.Markdown; // --------------------------------------------------------------------------------------------------------------------- @@ -19,266 +22,308 @@ public class IndividualMarkdownBenchmarks { [GlobalSetup] public void Setup() { var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddInfiniBlazor(); serviceCollection.AddLogging(); ServiceProvider provider = serviceCollection.BuildServiceProvider(); - + Parser = provider.GetRequiredService(); + + // Warmup to avoid first-hit cache effects + _ = Parser.Markdown.SerializeToSyntaxTree("warmup"); + } + + public sealed record BenchmarkCase(string Name, string Markdown) { + public override string ToString() => Name; } + [ParamsSource(nameof(Cases))] + public BenchmarkCase InputCase { get; set; } = null!; + // ----------------------------------------------------------------------------------------------------------------- - // Benchmarks for SinglelineStructuresRegex Features + // Data // ----------------------------------------------------------------------------------------------------------------- - [Benchmark] - public async Task EscapedCharacters() { - const string input = @"\*escaped text\*"; - - IMdSyntaxTree tree = Parser.Markdown.SerializeToSyntaxTree(input); - string? output = await Parser.Html.DeserializeToStringAsync(tree); - return output ?? throw new InvalidOperationException("The Markdown input should not be empty."); - } + public static IEnumerable Cases => [ + new("Paragraph_Base", "This is a paragraph."),// baseline - [Benchmark] - public async Task BoldAndItalic() { - const string input = "***bold and italic***"; - - IMdSyntaxTree tree = Parser.Markdown.SerializeToSyntaxTree(input); - string? output = await Parser.Html.DeserializeToStringAsync(tree); - return output ?? throw new InvalidOperationException("The Markdown input should not be empty."); - } + #region BlockQuote + new("BlockQuote_1Line", "> Blockquote text"), + new("BlockQuote_2Lines", """ + > Blockquote text + > Continued + """ + ), + new("BlockQuote_3Lines", """ + > A blockquote + > with multiple lines. + """ + ), + new("BlockQuote_10Lines", RepeatLines("> line", 10)), + new("BlockQuote_100Lines", RepeatLines("> line", 100)), + #endregion - [Benchmark] - public async Task BoldOnly() { - const string input = "**bold**"; - - IMdSyntaxTree tree = Parser.Markdown.SerializeToSyntaxTree(input); - string? output = await Parser.Html.DeserializeToStringAsync(tree); - return output ?? throw new InvalidOperationException("The Markdown input should not be empty."); - } + #region Bold + new("Bold", "**bold**"), + new("Bold_2InLine", "**bold** **again**"), + #endregion - [Benchmark] - public async Task ItalicOnly() { - const string input = "*italic*"; - - IMdSyntaxTree tree = Parser.Markdown.SerializeToSyntaxTree(input); - string? output = await Parser.Html.DeserializeToStringAsync(tree); - return output ?? throw new InvalidOperationException("The Markdown input should not be empty."); - } + #region Break + new("Break", "line
break"), + #endregion - [Benchmark] - public async Task Superscript() { - const string input = "^^sup-script^^"; - - IMdSyntaxTree tree = Parser.Markdown.SerializeToSyntaxTree(input); - string? output = await Parser.Html.DeserializeToStringAsync(tree); - return output ?? throw new InvalidOperationException("The Markdown input should not be empty."); - } + #region Callout + new("Callout", """ + >[!note] title + > body + """), + new("Callout_withoutBody", ">[!note] title"), + #endregion - [Benchmark] - public async Task Subscript() { - const string input = "^^sub-script^^"; - - IMdSyntaxTree tree = Parser.Markdown.SerializeToSyntaxTree(input); - string? output = await Parser.Html.DeserializeToStringAsync(tree); - return output ?? throw new InvalidOperationException("The Markdown input should not be empty."); - } + #region CodeBlock + new("CodeBlock", """ + ```csharp + public class MyClass { + public void MyMethod() { + Console.WriteLine("Hello, world!"); + } + } + ``` + """), + new("CodeBlock_NoLanguage", """ + ``` + something code related + ``` + """), + new("CodeBlock_50Lines", $""" + ```csharp + {RepeatLines("Console.WriteLine(\"Hello\");", 50)} + ``` + """), + new("CodeBlock_100Lines", $""" + ```csharp + {RepeatLines("Console.WriteLine(\"Hello\");", 100)} + ``` + """), + #endregion - [Benchmark] - public async Task Strikethrough() { - const string input = "~~strikethrough~~"; - - IMdSyntaxTree tree = Parser.Markdown.SerializeToSyntaxTree(input); - string? output = await Parser.Html.DeserializeToStringAsync(tree); - return output ?? throw new InvalidOperationException("The Markdown input should not be empty."); - } + #region CodeInline + new("CodeInline", "`code`"), + new("CodeInline_2ticks", "``code``"), + new("CodeInline_3ticks", "```code```"), + #endregion - [Benchmark] - public async Task Underline() { - const string input = "_underline_"; - - IMdSyntaxTree tree = Parser.Markdown.SerializeToSyntaxTree(input); - string? output = await Parser.Html.DeserializeToStringAsync(tree); - return output ?? throw new InvalidOperationException("The Markdown input should not be empty."); - } + #region Emote + new("Emote", ":flag-trans:"), + #endregion - [Benchmark] - public async Task InlineCode() { - const string input = "`inline code`"; - - IMdSyntaxTree tree = Parser.Markdown.SerializeToSyntaxTree(input); - string? output = await Parser.Html.DeserializeToStringAsync(tree); - return output ?? throw new InvalidOperationException("The Markdown input should not be empty."); - } + #region EscapedCharacters + new("EscapedCharacters", @"\*escaped italic\*"), + #endregion - [Benchmark] - public async Task Emotes() { - const string input = ":flag-trans:"; - - IMdSyntaxTree tree = Parser.Markdown.SerializeToSyntaxTree(input); - string? output = await Parser.Html.DeserializeToStringAsync(tree); - return output ?? throw new InvalidOperationException("The Markdown input should not be empty."); - } + #region Footnote + new("FootnoteReference", "[^1]"), + new("FootnoteDescription", "[^1]: footnote description"), + #endregion - [Benchmark] - public async Task NestedLinks() { - const string input = "[![nested link](image_url)](outer_url)"; - - IMdSyntaxTree tree = Parser.Markdown.SerializeToSyntaxTree(input); - string? output = await Parser.Html.DeserializeToStringAsync(tree); - return output ?? throw new InvalidOperationException("The Markdown input should not be empty."); - } + #region FrontMatter + new("FrontMatter", """ + --- + title: My Document + --- + """), + new("FrontMatter_2Entries", """ + --- + title: My Document + author: John Doe + --- + """), + #endregion - [Benchmark] - public async Task RegularLinks() { - const string input = "[Regular Link](https://example.com)"; - - IMdSyntaxTree tree = Parser.Markdown.SerializeToSyntaxTree(input); - string? output = await Parser.Html.DeserializeToStringAsync(tree); - return output ?? throw new InvalidOperationException("The Markdown input should not be empty."); - } + #region Heading + new("Heading_1", "# Header 1"), + new("Heading_2", "## Header 2"), + new("Heading_3", "### Header 3"), + new("Heading_4", "#### Header 4"), + new("Heading_5", "##### Header 5"), + new("Heading_6", "###### Header 6"), - [Benchmark] - public async Task Tags() { - const string input = "#tag"; - - IMdSyntaxTree tree = Parser.Markdown.SerializeToSyntaxTree(input); - string? output = await Parser.Html.DeserializeToStringAsync(tree); - return output ?? throw new InvalidOperationException("The Markdown input should not be empty."); - } + new("HeadingSimple", """ + heading + --- + """), + #endregion - [Benchmark] - public async Task HtmlSpecialCharacters() { - const string input = "© & < >"; - - IMdSyntaxTree tree = Parser.Markdown.SerializeToSyntaxTree(input); - string? output = await Parser.Html.DeserializeToStringAsync(tree); - return output ?? throw new InvalidOperationException("The Markdown input should not be empty."); - } + #region Highlight + new("Highlight", "==highlighted text=="), + #endregion - // ----------------------------------------------------------------------------------------------------------------- - // Benchmarks for MultilineStructuresRegex Features - // ----------------------------------------------------------------------------------------------------------------- + #region HorizontalRule + new("HorizontalRule", "---"), + #endregion - [Benchmark] - public async Task Headings() { - const string input = """ - # Header 1 - ## Header 2 - ### Header 3 - """; - - IMdSyntaxTree tree = Parser.Markdown.SerializeToSyntaxTree(input); - string? output = await Parser.Html.DeserializeToStringAsync(tree); - return output ?? throw new InvalidOperationException("The Markdown input should not be empty."); - } + #region HtmlBlock + new("HtmlBlock", "

HTML content

"), + #endregion - [Benchmark] - public async Task CodeBlocks() { - const string input = """ - ```csharp - code block - ``` - """; - - IMdSyntaxTree tree = Parser.Markdown.SerializeToSyntaxTree(input); - string? output = await Parser.Html.DeserializeToStringAsync(tree); - return output ?? throw new InvalidOperationException("The Markdown input should not be empty."); - } + #region Italic + new("Italic", "*italic*"), + new("Italic_2InLine", "*italic* *again*"), + #endregion - [Benchmark] - public async Task SimpleHeadings() { - const string input = """ - Simple Heading - --- - """; - - IMdSyntaxTree tree = Parser.Markdown.SerializeToSyntaxTree(input); - string? output = await Parser.Html.DeserializeToStringAsync(tree); - return output ?? throw new InvalidOperationException("The Markdown input should not be empty."); - } + #region Link + new("Link", "[Regular Link](https://example.com)"), + new("Link_Nested", "[![nested link](image_url)](outer_url)"), + #endregion - [Benchmark] - public async Task UnorderedLists() { - const string input = """ - - Item 1 - - Item 2 - - Nested Item - """; - - IMdSyntaxTree tree = Parser.Markdown.SerializeToSyntaxTree(input); - string? output = await Parser.Html.DeserializeToStringAsync(tree); - return output ?? throw new InvalidOperationException("The Markdown input should not be empty."); - } + #region List + new("List_UnOrdered", """ + - list item 1 + - list item 2 + """), + new("List_UnOrdered_50", RepeatLines("- item", 50)), + new("List_UnOrdered_100", RepeatLines("- item", 100)), - [Benchmark] - public async Task OrderedLists() { - const string input = """ - 1. First Item - 2. Second Item - - Nested Item - """; - - IMdSyntaxTree tree = Parser.Markdown.SerializeToSyntaxTree(input); - string? output = await Parser.Html.DeserializeToStringAsync(tree); - return output ?? throw new InvalidOperationException("The Markdown input should not be empty."); - } + new("List_Ordered", """ + 1. list item 1 + 2. list item 2 + """), + new("List_Ordered_50", RepeatLines("1. item", 50)), + new("List_Ordered_100", RepeatLines("1. item", 100)), - [Benchmark] - public async Task Tables() { - const string input = - """ - | Column 1 | Column 2 | - |----------|----------| - | Value 1 | Value 2 | - """; - - IMdSyntaxTree tree = Parser.Markdown.SerializeToSyntaxTree(input); - string? output = await Parser.Html.DeserializeToStringAsync(tree); - return output ?? throw new InvalidOperationException("The Markdown input should not be empty."); - } + new("List_Task", "- [ ] task item 1"), + new("List_Task_50", RepeatLines("- [ ] item", 50)), + new("List_Task_100", RepeatLines("- [ ] item", 100)), - [Benchmark] - public async Task BlockQuotes() { - const string input = """ - > A blockquote - > with multiple lines. - """; - - IMdSyntaxTree tree = Parser.Markdown.SerializeToSyntaxTree(input); - string? output = await Parser.Html.DeserializeToStringAsync(tree); - return output ?? throw new InvalidOperationException("The Markdown input should not be empty."); - } + new("List_Task_checked", "- [x] task item 1"), + new("List_Task_checked_50", RepeatLines("- [x] item", 50)), + new("List_Task_checked_100", RepeatLines("- [x] item", 100)), + #endregion - [Benchmark] - public async Task HtmlBlocks() { - const string input = "

HTML content

"; - - IMdSyntaxTree tree = Parser.Markdown.SerializeToSyntaxTree(input); - string? output = await Parser.Html.DeserializeToStringAsync(tree); - return output ?? throw new InvalidOperationException("The Markdown input should not be empty."); - } + #region NewLine + new("NewLine", """ + line 1 + line 2 + """), + #endregion - [Benchmark] - public async Task HorizontalRules() { - const string input = """ - --- + #region Paragraph + new("Paragraph", "This is a paragraph."), + #endregion - *** + #region Strikethrough + new("Strikethrough", "~~strikethrough~~"), + new("Strikethrough_2InLine", "~~strikethrough~~ ~~again~~"), + #endregion - ___ - """; - - IMdSyntaxTree tree = Parser.Markdown.SerializeToSyntaxTree(input); - string? output = await Parser.Html.DeserializeToStringAsync(tree); - return output ?? throw new InvalidOperationException("The Markdown input should not be empty."); - } + #region Subscript + new("Subscript", "~sub-script~"), + new("Subscript_2InLine", "~sub-script~ ~again~"), + #endregion + #region Superscript + new("Superscript", "^sup-script^"), + new("Superscript_2InLine", "^sup-script^ ^again^"), + #endregion + + #region Table + new("Table", """ + | Header 1 | Header 2 | + | -------- | -------- | + | Row 1 | Data 1 | + """), + new("Table_2Rows", """ + | Header 1 | Header 2 | + | -------- | -------- | + | Row 1 | Data 1 | + | Row 2 | Data 2 | + """), + new("Table_3Rows", """ + | Header 1 | Header 2 | + | -------- | -------- | + | Row 1 | Data 1 | + | Row 2 | Data 2 | + | Row 3 | Data 3 | + """), + new("Table_50Rows", $""" + | Header 1 | Header 2 | + | -------- | -------- | + {RepeatLines("| Row 1 | Data 1 |", 50)} + """), + new("Table_100Rows", $""" + | Header 1 | Header 2 | + | -------- | -------- | + {RepeatLines("| Row 1 | Data 1 |", 100)} + """), + #endregion + + #region Tag + new("Tag", "#tag"), + #endregion + + #region Template + new("Template", "{{template}}"), + #endregion + + #region Underline + new("Underline", "_underline_"), + #endregion + + #region User + new("User", "@user"), + #endregion + + #region WikiLink + new("WikiLink", "[[WikiLink]]"), + #endregion + + #region Wrapper + new("Wrapper", "<|color=red>red text"), + #endregion + + #region BoldAndItalic + new("BoldAndItalic", "***bold and italic***"), + new("BoldAndItalic_AfterEachOther", "**bold** and *italic*"), + #endregion + + #region Mixed + new("Mixed_RealWorld", """ + # Title + + Intro paragraph with *italic*, **bold**, and a [link](https://example.com). + + - item one + - item two + - nested item + + > A note block + > with multiple lines. + + ```csharp + public record Sample(int Id, string Name); + ``` + """), + #endregion + ]; + + // ----------------------------------------------------------------------------------------------------------------- + // Benchmarks + // ----------------------------------------------------------------------------------------------------------------- [Benchmark] - public async Task RemainderText() { - const string input = "This is normal text left over."; - - IMdSyntaxTree tree = Parser.Markdown.SerializeToSyntaxTree(input); - string? output = await Parser.Html.DeserializeToStringAsync(tree); - return output ?? throw new InvalidOperationException("The Markdown input should not be empty."); + public IMdSyntaxTree SerializeToSyntaxTree() { + IMdSyntaxTree tree = Parser.Markdown.SerializeToSyntaxTree(InputCase.Markdown); + return tree; + } + + // ----------------------------------------------------------------------------------------------------------------- + // Helpers + // ----------------------------------------------------------------------------------------------------------------- + private static string RepeatLines(string line, int count) { + var sb = new StringBuilder(); + for (int i = 0; i < count; i++) { + sb.AppendLine(line); + } + + return sb.ToString(); } } diff --git a/benchmarks/Benchmarks.InfiniBlazor.Markdown/MarkdownBenchmarks.cs b/benchmarks/Benchmarks.InfiniBlazor.Markdown/MarkdownBenchmarks.cs index 8e1f2b44..680840bb 100644 --- a/benchmarks/Benchmarks.InfiniBlazor.Markdown/MarkdownBenchmarks.cs +++ b/benchmarks/Benchmarks.InfiniBlazor.Markdown/MarkdownBenchmarks.cs @@ -35,6 +35,8 @@ public async Task Setup() { private static ServiceProvider CreateProvider(Action? configure = null) { var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddInfiniBlazor(config => configure?.Invoke(config.Markdown)); serviceCollection.AddLogging(); return serviceCollection.BuildServiceProvider(); diff --git a/benchmarks/Benchmarks.InfiniBlazor.Markdown/MockJsRuntime.cs b/benchmarks/Benchmarks.InfiniBlazor.Markdown/MockJsRuntime.cs new file mode 100644 index 00000000..f562e2e8 --- /dev/null +++ b/benchmarks/Benchmarks.InfiniBlazor.Markdown/MockJsRuntime.cs @@ -0,0 +1,13 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +using Microsoft.JSInterop; + +namespace Benchmarks.InfiniBlazor.Markdown; +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +public class MockJsRuntime : IJSRuntime { + public ValueTask InvokeAsync(string identifier, object?[]? args) => default; + public ValueTask InvokeAsync(string identifier, CancellationToken cancellationToken, object?[]? args) => default; +} diff --git a/benchmarks/Benchmarks.InfiniBlazor.Markdown/MockNavigationManager.cs b/benchmarks/Benchmarks.InfiniBlazor.Markdown/MockNavigationManager.cs new file mode 100644 index 00000000..e17b8c5e --- /dev/null +++ b/benchmarks/Benchmarks.InfiniBlazor.Markdown/MockNavigationManager.cs @@ -0,0 +1,13 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +using Microsoft.AspNetCore.Components; + +namespace Benchmarks.InfiniBlazor.Markdown; +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +public class MockNavigationManager : NavigationManager { + public MockNavigationManager() => Initialize("http://localhost/", "http://localhost/"); + protected override void NavigateToCore(string uri, bool forceLoad) { } +} diff --git a/benchmarks/Benchmarks.InfiniBlazor.Markdown/Program.cs b/benchmarks/Benchmarks.InfiniBlazor.Markdown/Program.cs index dd5bf3cf..7aba74ce 100644 --- a/benchmarks/Benchmarks.InfiniBlazor.Markdown/Program.cs +++ b/benchmarks/Benchmarks.InfiniBlazor.Markdown/Program.cs @@ -9,7 +9,7 @@ namespace Benchmarks.InfiniBlazor.Markdown; // --------------------------------------------------------------------------------------------------------------------- public static class Program { public static void Main(string[] args) { - BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); + BenchmarkRunner.Run(); } } diff --git a/benchmarks/Benchmarks.InfiniBlazor.Markdown/Results.md b/benchmarks/Benchmarks.InfiniBlazor.Markdown/Results.md new file mode 100644 index 00000000..1710ee84 --- /dev/null +++ b/benchmarks/Benchmarks.InfiniBlazor.Markdown/Results.md @@ -0,0 +1,83 @@ +# General Benchmark +| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Gen2 | Allocated | Alloc Ratio | +|----------------|---------:|----------:|----------:|------:|--------:|---------:|---------:|--------:|----------:|------------:| +| RenderMarkdown | 2.256 ms | 0.0420 ms | 0.1076 ms | 1.00 | 0.07 | 257.8125 | 250.0000 | 46.8750 | 2.14 MB | 1.00 | + + +# Individual Benchmarks + +| Method | InputCase | Mean | Error | StdDev | Median | Gen0 | Gen1 | Gen2 | Allocated | +|-----------------------|----------------------|-------------:|------------:|-------------:|-------------:|--------:|-------:|-------:|----------:| +| SerializeToSyntaxTree | BlockQuote_100Lines | 104,823.0 ns | 2,087.82 ns | 4,022.52 ns | 105,504.4 ns | 13.0615 | 5.0049 | - | 107.17 KB | +| SerializeToSyntaxTree | BlockQuote_10Lines | 11,833.7 ns | 235.91 ns | 394.16 ns | 11,803.4 ns | 1.5259 | 0.0763 | - | 12.49 KB | +| SerializeToSyntaxTree | BlockQuote_1Line | 1,738.4 ns | 33.07 ns | 35.38 ns | 1,730.8 ns | 0.3033 | 0.0038 | - | 2.48 KB | +| SerializeToSyntaxTree | BlockQuote_2Lines | 4,276.1 ns | 366.70 ns | 1,081.21 ns | 4,119.1 ns | 0.4463 | 0.0076 | - | 3.67 KB | +| SerializeToSyntaxTree | BlockQuote_3Lines | 3,461.2 ns | 63.81 ns | 145.32 ns | 3,457.7 ns | 0.4539 | 0.0076 | - | 3.73 KB | +| SerializeToSyntaxTree | Bold | 1,797.7 ns | 35.52 ns | 46.19 ns | 1,804.6 ns | 0.3242 | 0.0038 | - | 2.66 KB | +| SerializeToSyntaxTree | BoldAndItalic | 2,632.7 ns | 38.95 ns | 69.24 ns | 2,645.4 ns | 0.4196 | 0.0076 | - | 3.45 KB | +| SerializeToSyntaxTree | BoldA(...)Other [28] | 3,086.9 ns | 59.35 ns | 70.65 ns | 3,085.1 ns | 0.4425 | 0.0076 | - | 3.62 KB | +| SerializeToSyntaxTree | Bold_2InLine | 2,836.9 ns | 47.08 ns | 44.04 ns | 2,852.4 ns | 0.4425 | 0.0076 | - | 3.63 KB | +| SerializeToSyntaxTree | Break | 2,049.5 ns | 39.98 ns | 57.34 ns | 2,060.3 ns | 0.2899 | 0.0038 | - | 2.38 KB | +| SerializeToSyntaxTree | Callout | 2,797.3 ns | 55.42 ns | 75.86 ns | 2,813.5 ns | 0.4463 | 0.0076 | - | 3.67 KB | +| SerializeToSyntaxTree | Callout_withoutBody | 1,509.3 ns | 29.77 ns | 42.69 ns | 1,509.6 ns | 0.3185 | 0.0038 | - | 2.6 KB | +| SerializeToSyntaxTree | CodeBlock | 2,275.9 ns | 45.41 ns | 105.26 ns | 2,271.9 ns | 0.3090 | 0.0038 | - | 2.55 KB | +| SerializeToSyntaxTree | CodeBlock_100Lines | 40,663.0 ns | 1,476.87 ns | 4,261.10 ns | 39,504.6 ns | 1.5869 | 0.0610 | - | 13.09 KB | +| SerializeToSyntaxTree | CodeBlock_50Lines | 21,371.0 ns | 500.93 ns | 1,453.28 ns | 21,215.9 ns | 0.9155 | 0.0305 | - | 7.62 KB | +| SerializeToSyntaxTree | CodeBlock_NoLanguage | 1,164.9 ns | 44.62 ns | 131.57 ns | 1,163.4 ns | 0.2670 | 0.0038 | - | 2.19 KB | +| SerializeToSyntaxTree | CodeInline | 1,330.2 ns | 26.01 ns | 42.01 ns | 1,338.5 ns | 0.3014 | 0.0038 | - | 2.47 KB | +| SerializeToSyntaxTree | CodeInline_2ticks | 1,368.1 ns | 24.08 ns | 52.34 ns | 1,355.8 ns | 0.3014 | 0.0038 | - | 2.47 KB | +| SerializeToSyntaxTree | CodeInline_3ticks | 1,363.9 ns | 20.05 ns | 44.01 ns | 1,365.1 ns | 0.3014 | 0.0038 | - | 2.47 KB | +| SerializeToSyntaxTree | Emote | 1,187.8 ns | 23.25 ns | 34.80 ns | 1,199.8 ns | 0.2365 | 0.0019 | - | 1.94 KB | +| SerializeToSyntaxTree | EscapedCharacters | 1,768.0 ns | 34.07 ns | 48.86 ns | 1,789.2 ns | 0.3147 | 0.0038 | - | 2.57 KB | +| SerializeToSyntaxTree | FootnoteDescription | 1,716.7 ns | 17.13 ns | 15.19 ns | 1,722.4 ns | 0.3357 | 0.0038 | - | 2.75 KB | +| SerializeToSyntaxTree | FootnoteReference | 1,201.7 ns | 22.91 ns | 22.50 ns | 1,202.1 ns | 0.2842 | 0.0038 | - | 2.34 KB | +| SerializeToSyntaxTree | FrontMatter | 763.8 ns | 11.74 ns | 10.98 ns | 762.4 ns | 0.2518 | 0.0010 | - | 2.06 KB | +| SerializeToSyntaxTree | FrontMatter_2Entries | 3,521.8 ns | 67.13 ns | 74.61 ns | 3,513.4 ns | 0.5608 | 0.0114 | - | 4.61 KB | +| SerializeToSyntaxTree | HeadingSimple | 971.1 ns | 19.26 ns | 20.61 ns | 978.1 ns | 0.2651 | 0.0038 | - | 2.18 KB | +| SerializeToSyntaxTree | Heading_1 | 931.7 ns | 18.63 ns | 17.43 ns | 932.3 ns | 0.2565 | 0.0029 | - | 2.1 KB | +| SerializeToSyntaxTree | Heading_2 | 1,009.0 ns | 31.89 ns | 91.49 ns | 982.4 ns | 0.2565 | 0.0029 | - | 2.1 KB | +| SerializeToSyntaxTree | Heading_3 | 1,002.5 ns | 19.47 ns | 29.13 ns | 1,006.0 ns | 0.2556 | 0.0019 | - | 2.1 KB | +| SerializeToSyntaxTree | Heading_4 | 1,023.8 ns | 19.41 ns | 37.40 ns | 1,022.5 ns | 0.2556 | 0.0019 | - | 2.1 KB | +| SerializeToSyntaxTree | Heading_5 | 1,039.7 ns | 20.60 ns | 38.70 ns | 1,029.7 ns | 0.2556 | 0.0019 | - | 2.1 KB | +| SerializeToSyntaxTree | Heading_6 | 1,048.9 ns | 21.03 ns | 27.35 ns | 1,045.3 ns | 0.2556 | 0.0019 | - | 2.1 KB | +| SerializeToSyntaxTree | Highlight | 2,515.1 ns | 157.21 ns | 463.55 ns | 2,493.6 ns | 0.3166 | 0.0038 | - | 2.59 KB | +| SerializeToSyntaxTree | HorizontalRule | 971.5 ns | 65.16 ns | 192.12 ns | 956.4 ns | 0.2079 | 0.0019 | - | 1.71 KB | +| SerializeToSyntaxTree | HtmlBlock | 2,211.7 ns | 51.54 ns | 146.21 ns | 2,205.3 ns | 0.3262 | 0.0038 | - | 2.66 KB | +| SerializeToSyntaxTree | Italic | 2,160.8 ns | 74.38 ns | 213.42 ns | 2,109.4 ns | 0.3281 | 0.0038 | - | 2.7 KB | +| SerializeToSyntaxTree | Italic_2InLine | 3,939.5 ns | 253.46 ns | 747.33 ns | 3,960.8 ns | 0.4501 | 0.0076 | - | 3.7 KB | +| SerializeToSyntaxTree | Link | 3,508.9 ns | 124.84 ns | 368.10 ns | 3,442.3 ns | 0.3662 | - | - | 3.02 KB | +| SerializeToSyntaxTree | Link_Nested | 5,467.7 ns | 108.85 ns | 214.85 ns | 5,483.7 ns | 0.3586 | - | - | 2.97 KB | +| SerializeToSyntaxTree | List_Ordered | 3,610.1 ns | 71.50 ns | 189.61 ns | 3,616.5 ns | 0.5646 | 0.0153 | - | 4.67 KB | +| SerializeToSyntaxTree | List_Ordered_100 | 124,239.3 ns | 6,202.89 ns | 18,289.38 ns | 129,261.8 ns | 17.0898 | 8.0566 | - | 139.82 KB | +| SerializeToSyntaxTree | List_Ordered_50 | 66,607.1 ns | 1,329.86 ns | 3,640.46 ns | 66,409.9 ns | 8.6060 | 2.1973 | - | 70.49 KB | +| SerializeToSyntaxTree | List_Task | 2,385.7 ns | 119.07 ns | 351.09 ns | 2,549.2 ns | 0.3967 | 0.0076 | - | 3.27 KB | +| SerializeToSyntaxTree | List_Task_100 | 125,710.5 ns | 4,045.63 ns | 11,542.41 ns | 128,810.2 ns | 17.2119 | 7.9346 | - | 140.99 KB | +| SerializeToSyntaxTree | List_Task_50 | 70,300.3 ns | 1,404.52 ns | 1,875.00 ns | 69,753.2 ns | 8.6670 | 2.3193 | - | 71.08 KB | +| SerializeToSyntaxTree | List_Task_checked | 2,762.5 ns | 54.61 ns | 95.64 ns | 2,777.2 ns | 0.3967 | 0.0076 | - | 3.27 KB | +| SerializeToSyntaxTree | List_(...)d_100 [21] | 119,430.4 ns | 2,377.28 ns | 5,964.13 ns | 120,232.6 ns | 17.0898 | 7.8125 | - | 141 KB | +| SerializeToSyntaxTree | List_Task_checked_50 | 71,094.4 ns | 1,323.01 ns | 2,484.94 ns | 70,931.7 ns | 8.6670 | 2.3193 | - | 71.08 KB | +| SerializeToSyntaxTree | List_UnOrdered | 4,420.9 ns | 127.70 ns | 376.53 ns | 4,502.9 ns | 0.5569 | 0.0153 | - | 4.56 KB | +| SerializeToSyntaxTree | List_UnOrdered_100 | 128,540.7 ns | 2,962.19 ns | 8,687.59 ns | 128,347.2 ns | 16.3574 | 7.5684 | - | 133.96 KB | +| SerializeToSyntaxTree | List_UnOrdered_50 | 65,977.7 ns | 2,163.15 ns | 6,378.09 ns | 65,821.2 ns | 8.1787 | 1.9531 | - | 67.56 KB | +| SerializeToSyntaxTree | Mixed_RealWorld | 29,987.4 ns | 596.90 ns | 1,572.48 ns | 29,819.9 ns | 2.1362 | 0.1221 | - | 17.84 KB | +| SerializeToSyntaxTree | NewLine | 4,136.9 ns | 78.82 ns | 93.83 ns | 4,129.8 ns | 0.3662 | - | - | 3.03 KB | +| SerializeToSyntaxTree | Paragraph | 2,360.7 ns | 47.03 ns | 117.13 ns | 2,358.6 ns | 0.2365 | - | - | 1.94 KB | +| SerializeToSyntaxTree | Paragraph_Base | 2,063.3 ns | 117.09 ns | 345.23 ns | 2,197.4 ns | 0.2365 | - | - | 1.94 KB | +| SerializeToSyntaxTree | Strikethrough | 3,232.4 ns | 96.97 ns | 285.91 ns | 3,244.3 ns | 0.3242 | 0.0038 | - | 2.67 KB | +| SerializeToSyntaxTree | Strik(...)nLine [21] | 5,440.2 ns | 107.91 ns | 236.86 ns | 5,429.4 ns | 0.4349 | 0.0076 | - | 3.59 KB | +| SerializeToSyntaxTree | Subscript | 2,684.4 ns | 53.44 ns | 90.74 ns | 2,678.2 ns | 0.3281 | 0.0038 | - | 2.7 KB | +| SerializeToSyntaxTree | Subscript_2InLine | 4,477.2 ns | 89.46 ns | 131.13 ns | 4,480.8 ns | 0.4501 | 0.0076 | - | 3.7 KB | +| SerializeToSyntaxTree | Superscript | 2,762.5 ns | 54.95 ns | 154.98 ns | 2,734.4 ns | 0.3281 | 0.0038 | - | 2.7 KB | +| SerializeToSyntaxTree | Superscript_2InLine | 5,117.6 ns | 102.96 ns | 303.57 ns | 5,181.1 ns | 0.4501 | 0.0076 | - | 3.7 KB | +| SerializeToSyntaxTree | Table | 5,861.7 ns | 134.17 ns | 395.61 ns | 5,825.8 ns | 0.5035 | 0.0076 | - | 4.13 KB | +| SerializeToSyntaxTree | Table_100Rows | 142,121.2 ns | 5,273.80 ns | 15,549.92 ns | 143,746.1 ns | 12.4512 | 2.1973 | 0.9766 | 102.68 KB | +| SerializeToSyntaxTree | Table_2Rows | 7,917.2 ns | 347.31 ns | 1,024.04 ns | 7,996.3 ns | 0.6256 | 0.0153 | - | 5.13 KB | +| SerializeToSyntaxTree | Table_3Rows | 7,426.9 ns | 396.97 ns | 1,157.99 ns | 7,125.2 ns | 0.7477 | 0.0153 | - | 6.12 KB | +| SerializeToSyntaxTree | Table_50Rows | 50,289.8 ns | 1,459.92 ns | 4,235.50 ns | 48,934.8 ns | 6.4697 | 0.6104 | - | 53.04 KB | +| SerializeToSyntaxTree | Tag | 1,178.3 ns | 23.45 ns | 47.37 ns | 1,180.6 ns | 0.2861 | 0.0038 | - | 2.34 KB | +| SerializeToSyntaxTree | Template | 1,530.5 ns | 45.19 ns | 127.46 ns | 1,494.4 ns | 0.3090 | 0.0038 | - | 2.53 KB | +| SerializeToSyntaxTree | Underline | 1,846.8 ns | 46.27 ns | 132.00 ns | 1,817.0 ns | 0.3300 | 0.0038 | - | 2.7 KB | +| SerializeToSyntaxTree | User | 1,271.7 ns | 41.44 ns | 118.24 ns | 1,233.1 ns | 0.2861 | 0.0038 | - | 2.34 KB | +| SerializeToSyntaxTree | WikiLink | 1,352.8 ns | 23.71 ns | 19.80 ns | 1,358.0 ns | 0.2861 | 0.0038 | - | 2.35 KB | +| SerializeToSyntaxTree | Wrapper | 2,165.3 ns | 59.70 ns | 171.29 ns | 2,124.9 ns | 0.3662 | 0.0038 | - | 3 KB | + diff --git a/docs/InfiniBlazorDocs/Program.cs b/docs/InfiniBlazorDocs/Program.cs index 4d30b29f..4e7faf05 100644 --- a/docs/InfiniBlazorDocs/Program.cs +++ b/docs/InfiniBlazorDocs/Program.cs @@ -1,16 +1,12 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- -using InfiniBlazorDocs.Services; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Serilog; using Serilog.Extensions.Logging; -#if DEBUG -#endif - namespace InfiniBlazorDocs; // --------------------------------------------------------------------------------------------------------------------- // Code diff --git a/failure_footnote.txt b/failure_footnote.txt new file mode 100644 index 00000000..4c8b6bee Binary files /dev/null and b/failure_footnote.txt differ diff --git a/full_test_output.txt b/full_test_output.txt new file mode 100644 index 00000000..7467923a Binary files /dev/null and b/full_test_output.txt differ diff --git a/package-lock.json b/package-lock.json index 0dda466f..a8e3c034 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "infinilore", + "name": "infiniblazor", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "infinilore", + "name": "infiniblazor", "version": "0.1.0", "license": "GNUv3", "dependencies": { @@ -995,7 +995,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1022,7 +1021,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -1123,7 +1121,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2372,8 +2369,7 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tapable": { "version": "2.3.0", @@ -2491,7 +2487,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2558,7 +2553,6 @@ "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -2608,7 +2602,6 @@ "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.6.1", "@webpack-cli/configtest": "^3.0.1", diff --git a/src/InfiniBlazor.Core.Markdown.Components/InfiniMarkdownEditor/InfiniMarkdownEditor.razor b/src/InfiniBlazor.Core.Markdown.Components/InfiniMarkdownEditor/InfiniMarkdownEditor.razor index 9cce9ed9..19322234 100644 --- a/src/InfiniBlazor.Core.Markdown.Components/InfiniMarkdownEditor/InfiniMarkdownEditor.razor +++ b/src/InfiniBlazor.Core.Markdown.Components/InfiniMarkdownEditor/InfiniMarkdownEditor.razor @@ -5,7 +5,6 @@ @using InfiniBlazor.Markdown.Parsers.Blazor @using InfiniBlazor.Markdown.Syntax @using InfiniBlazor.TextEditor -@* @using Microsoft.Extensions.Logging *@ @namespace InfiniBlazor.Markdown @* ------------------------------------------------------------------------------------------------------------------ *@ @@ -112,7 +111,7 @@ protected override async Task OnParametersSetAsync() { await base.OnParametersSetAsync(); - EditorContext.SyntaxTree.ReturnToPool(); + MdSyntaxTreePool.Shared.Return(EditorContext.SyntaxTree); EditorContext.SyntaxTree = Parser.Markdown.SerializeToSyntaxTree(EditorContext.TextSource.Text); await EditorContext.InvokeSyntaxTreeChangeAsync(); await InvokeAsync(StateHasChanged); @@ -147,7 +146,7 @@ if (EditorContext.TextSource.IsEmpty) await InfiniBlazorJs.Element.ClearValueAsync(EditorContext.InputElementRef); else await InfiniBlazorJs.Element.SetValueSelectionAwareAsync(EditorContext.InputElementRef, EditorContext.TextSource.Text); - EditorContext.SyntaxTree.ReturnToPool(); + MdSyntaxTreePool.Shared.Return(EditorContext.SyntaxTree); EditorContext.SyntaxTree = Parser.Markdown.SerializeToSyntaxTree(EditorContext.TextSource.Text); await EditorContext.InvokeSyntaxTreeChangeAsync(); diff --git a/src/InfiniBlazor.Core.Markdown.Components/MdBlazorComponents/MdInfiniCallout.razor b/src/InfiniBlazor.Core.Markdown.Components/MdBlazorComponents/MdInfiniCallout.razor index e7cb26ad..e77c13e3 100644 --- a/src/InfiniBlazor.Core.Markdown.Components/MdBlazorComponents/MdInfiniCallout.razor +++ b/src/InfiniBlazor.Core.Markdown.Components/MdBlazorComponents/MdInfiniCallout.razor @@ -1,7 +1,6 @@ @* ------------------------------------------------------------------------------------------------------------------ *@ @* Imports @* ------------------------------------------------------------------------------------------------------------------ *@ -@using InfiniBlazor.Markdown.Syntax @using InfiniBlazor.Markdown.Parsers.Blazor @using InfiniBlazor.Markdown.Syntax.Nodes diff --git a/src/InfiniBlazor.Core.Markdown.Components/MdBlazorComponents/MdInfiniImage.razor b/src/InfiniBlazor.Core.Markdown.Components/MdBlazorComponents/MdInfiniImage.razor index b3e13f72..d87fdabb 100644 --- a/src/InfiniBlazor.Core.Markdown.Components/MdBlazorComponents/MdInfiniImage.razor +++ b/src/InfiniBlazor.Core.Markdown.Components/MdBlazorComponents/MdInfiniImage.razor @@ -1,7 +1,6 @@ @* ------------------------------------------------------------------------------------------------------------------ *@ @* Imports @* ------------------------------------------------------------------------------------------------------------------ *@ -@using InfiniBlazor.Markdown.Syntax @using InfiniBlazor.Markdown.Parsers.Blazor @using InfiniBlazor.Markdown.Syntax.Nodes diff --git a/src/InfiniBlazor.Core.Markdown.Components/MdBlazorComponents/MdInfiniLink.razor b/src/InfiniBlazor.Core.Markdown.Components/MdBlazorComponents/MdInfiniLink.razor index 7e1cb823..132d6130 100644 --- a/src/InfiniBlazor.Core.Markdown.Components/MdBlazorComponents/MdInfiniLink.razor +++ b/src/InfiniBlazor.Core.Markdown.Components/MdBlazorComponents/MdInfiniLink.razor @@ -2,7 +2,6 @@ @* Imports @* ------------------------------------------------------------------------------------------------------------------ *@ @using InfiniBlazor.Markdown.Parsers.Blazor -@using InfiniBlazor.Markdown.Syntax @using InfiniBlazor.Markdown.Syntax.Nodes @namespace InfiniBlazor.Markdown.MdBlazorComponents diff --git a/src/InfiniBlazor.Core.Markdown.Components/MdBlazorComponents/MdInfiniWrapper.razor b/src/InfiniBlazor.Core.Markdown.Components/MdBlazorComponents/MdInfiniWrapper.razor index 4b6bfbfa..726b7c2e 100644 --- a/src/InfiniBlazor.Core.Markdown.Components/MdBlazorComponents/MdInfiniWrapper.razor +++ b/src/InfiniBlazor.Core.Markdown.Components/MdBlazorComponents/MdInfiniWrapper.razor @@ -2,7 +2,6 @@ @* Imports @* ------------------------------------------------------------------------------------------------------------------ *@ @using InfiniBlazor.Markdown.Parsers.Blazor -@using InfiniBlazor.Markdown.Syntax @using InfiniBlazor.Markdown.Syntax.Nodes @namespace InfiniBlazor.Markdown.MdBlazorComponents diff --git a/src/InfiniBlazor.Core.Markdown/Config/InfiniBlazorMarkdownConfig.cs b/src/InfiniBlazor.Core.Markdown/Config/InfiniBlazorMarkdownConfig.cs index 17694bd2..5826877d 100644 --- a/src/InfiniBlazor.Core.Markdown/Config/InfiniBlazorMarkdownConfig.cs +++ b/src/InfiniBlazor.Core.Markdown/Config/InfiniBlazorMarkdownConfig.cs @@ -4,6 +4,8 @@ using InfiniBlazor.Config; using InfiniBlazor.Markdown.Editors; using InfiniBlazor.Markdown.Parsers.Blazor; +using InfiniBlazor.Markdown.Parsers.Markdown.Serializer; +using InfiniBlazor.Markdown.Parsers.Markdown.Serializer.NodeSerializers; using InfiniBlazor.Markdown.Syntax; using Microsoft.Extensions.DependencyInjection; using System.Collections.Frozen; @@ -32,6 +34,49 @@ public InfiniBlazorMarkdownConfig(IServiceCollection serviceCollection) { serviceCollection.AddSingleton(TextEditorFactory.CreateTextEditor); serviceCollection.AddSingleton(this); + serviceCollection.AddSingleton(sp => { + var fullOptions = new MarkdownSerializerOptions { + SingleLine = [ + new EscapedCharacterSyntaxNodeSerializer(), + new BoldSyntaxNodeSerializer(), + new ItalicSyntaxNodeSerializer(), + new SuperScriptSyntaxNodeSerializer(), + new SubScriptSyntaxNodeSerializer(), + new CodeInlineSyntaxNodeSerializer(), + new StrikeSyntaxNodeSerializer(), + new UnderlineSyntaxNodeSerializer(), + new HighlightSyntaxNodeSerializer(), + new EmoteSyntaxNodeSerializer(), + new WikiLinkSyntaxNodeSerializer(), + new TemplateSyntaxNodeSerializer(), + new LinkSyntaxNodeSerializer(), + new TagSyntaxNodeSerializer(), + new UserSyntaxNodeSerializer(), + new FootnoteReferenceSyntaxNodeSerializer(), + new WrapperSyntaxNodeSerializer(), + new BreakSyntaxNodeSerializer(), + ], + MultiLine = [ + new HeadingSyntaxNodeSerializer(), + new CodeBlockSyntaxNodeSerializer(), + new HeadingSimpleSyntaxNodeSerializer(), + new ListSyntaxNodeSerializer(), + new TableSyntaxNodeSerializer(), + new CalloutSyntaxNodeSerializer(), + new BlockQuoteSyntaxNodeSerializer(), + new FootnoteDescriptionSyntaxNodeSerializer(), + new HtmlBlockSyntaxNodeSerializer(), + new HorizontalRuleSyntaxNodeSerializer(), + new ParagraphSyntaxNodeSerializer(), + new NewLineSyntaxNodeSerializer() + ], + FrontMatter = new FrontmatterSyntaxNodeSerializer() + }; + + var factory = sp.GetRequiredService(); + return factory.Create(fullOptions); + }); + ComponentRecordsLazy = new Lazy>(() => { ComponentRecords.TrimExcess(); return ComponentRecords.ToFrozenDictionary( diff --git a/src/InfiniBlazor.Core.Markdown/Parsers/Json/JsonMdSyntaxNodeVisitor.cs b/src/InfiniBlazor.Core.Markdown/Parsers/Json/JsonMdSyntaxNodeVisitor.cs index 0b24e713..4356285b 100644 --- a/src/InfiniBlazor.Core.Markdown/Parsers/Json/JsonMdSyntaxNodeVisitor.cs +++ b/src/InfiniBlazor.Core.Markdown/Parsers/Json/JsonMdSyntaxNodeVisitor.cs @@ -48,7 +48,7 @@ protected virtual void DeserializeDetails(TNode node, Utf8JsonWriter writer) { } public IMdSyntaxNode SerializeToNode(JsonElement element, IMdSyntaxNode parentNode) { - TNode node = MdSyntaxNode.Pool.Get(); + TNode node = MdSyntaxNodePool.Shared.Get(); parentNode.AddChildNode(node); SerializeDetails(element, node); diff --git a/src/InfiniBlazor.Core.Markdown/Parsers/Json/JsonMdSyntaxTreeParser.cs b/src/InfiniBlazor.Core.Markdown/Parsers/Json/JsonMdSyntaxTreeParser.cs index 85ec3d78..1ee19511 100644 --- a/src/InfiniBlazor.Core.Markdown/Parsers/Json/JsonMdSyntaxTreeParser.cs +++ b/src/InfiniBlazor.Core.Markdown/Parsers/Json/JsonMdSyntaxTreeParser.cs @@ -81,7 +81,7 @@ public JsonMdSyntaxTreeParser() { // ----------------------------------------------------------------------------------------------------------------- #region Deserialize public string DeserializeToString(IMdSyntaxTree input) { - var element = DeserializeToJsonElement(input); + JsonElement element = DeserializeToJsonElement(input); return JsonSerializer.Serialize(element, SerializerOptions); } @@ -102,7 +102,7 @@ public JsonElement DeserializeToJsonElement(IMdSyntaxTree tree) { writer.Flush(); stream.Position = 0; - using var document = JsonDocument.Parse(stream); + using JsonDocument document = JsonDocument.Parse(stream); return document.RootElement.Clone(); } @@ -131,7 +131,7 @@ private void DeserializeNode(IMdSyntaxNode node, Utf8JsonWriter writer) { visitor.DeserializeToJson(node, writer); } - var children = node.GetChildren().ToList(); + List children = node.GetChildren().ToList(); if (children.Count > 0) { writer.WriteStartArray("children"); foreach (IMdSyntaxNode child in children) { @@ -146,13 +146,13 @@ private void DeserializeNode(IMdSyntaxNode node, Utf8JsonWriter writer) { #region Serialize public IMdSyntaxTree SerializeToSyntaxTree(JsonElement element) { - if (!element.TryGetProperty("type", out var typeProperty) || typeProperty.GetString() != "MdSyntaxTree") { + if (!element.TryGetProperty("type", out JsonElement typeProperty) || typeProperty.GetString() != "MdSyntaxTree") { throw new InvalidOperationException("Invalid JSON root element"); } MdSyntaxTree tree = new(); - if (element.TryGetProperty("children", out var childrenProperty) && childrenProperty.ValueKind == JsonValueKind.Array) { + if (element.TryGetProperty("children", out JsonElement childrenProperty) && childrenProperty.ValueKind == JsonValueKind.Array) { foreach (JsonElement child in childrenProperty.EnumerateArray()) { SerializeNode(child, tree.RootNode); } @@ -164,7 +164,7 @@ public IMdSyntaxTree SerializeToSyntaxTree(JsonElement element) { public async Task SerializeToSyntaxTreeAsync(Stream stream, CancellationToken ct = default) { ArgumentNullException.ThrowIfNull(stream); - using var document = await JsonDocument.ParseAsync(stream, cancellationToken: ct); + using JsonDocument document = await JsonDocument.ParseAsync(stream, cancellationToken: ct); return SerializeToSyntaxTree(document.RootElement); } @@ -177,15 +177,15 @@ public async Task SerializeToSyntaxTreeAsync(string filePath, Can } private void SerializeNode(JsonElement element, IMdSyntaxNode parentNode) { - if (element.TryGetProperty("type", out var typeProperty)) { + if (element.TryGetProperty("type", out JsonElement typeProperty)) { string? typeName = typeProperty.GetString(); if (!string.IsNullOrWhiteSpace(typeName) && _nodeTypes.TryGetValue(typeName, out Type? nodeType) && _visitors.TryGetValue(nodeType, out IJsonMdSyntaxNodeVisitor? visitor)) { - var newNode = visitor.SerializeToNode(element, parentNode); + IMdSyntaxNode newNode = visitor.SerializeToNode(element, parentNode); - if (element.TryGetProperty("children", out var childrenProperty) && childrenProperty.ValueKind == JsonValueKind.Array) { + if (element.TryGetProperty("children", out JsonElement childrenProperty) && childrenProperty.ValueKind == JsonValueKind.Array) { foreach (JsonElement child in childrenProperty.EnumerateArray()) { SerializeNode(child, newNode); } diff --git a/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/MdSerializerFactory.cs b/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/MdSerializerFactory.cs new file mode 100644 index 00000000..4ab39513 --- /dev/null +++ b/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/MdSerializerFactory.cs @@ -0,0 +1,61 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +using CodeOfChaos.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System.Buffers; +using System.Collections.Immutable; + +namespace InfiniBlazor.Markdown.Parsers.Markdown.Serializer; +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +[InjectableSingleton] +public class MdStringMdSyntaxSerializerFactory(ILogger logger) : IMdSerializerFactory { + + // ----------------------------------------------------------------------------------------------------------------- + // Methods + // ----------------------------------------------------------------------------------------------------------------- + public IMdStringMdSyntaxSerializer Create(MarkdownSerializerOptions options) { + // ReSharper disable twice UseCollectionExpression + ImmutableArray singleLineSerializers = options.SingleLine.ToImmutableArray(); + ImmutableArray[] singleLineLookup = BuildLookup(singleLineSerializers); + SearchValues singleLineTriggerSearchValues = SearchValues.Create(options.SingleLine + .SelectMany(s => s.TriggerCharacters) + .Distinct() + .ToArray()); + + ImmutableArray multiLineSerializers = options.MultiLine.ToImmutableArray(); + ImmutableArray[] multiLineLookup = BuildLookup(multiLineSerializers); + + return new MdStringMdSyntaxSerializer(logger) { + SingleLineSerializers = singleLineSerializers, + SingleLineLookup = singleLineLookup, + SingleLineTriggerSearchValues = singleLineTriggerSearchValues, + + MultiLineSerializers = multiLineSerializers, + MultiLineLookup = multiLineLookup, + + FrontMatterSerializer = options.FrontMatter + }; + } + + private static ImmutableArray[] BuildLookup(ImmutableArray serializers) { + var table = new ImmutableArray[256]; + + // Identify "Global" serializers (those with no triggers) + IMdSyntaxNodeSerializer[] globals = serializers.Where(s => s.TriggerCharacters.IsEmpty()).ToArray(); + + for (int i = 0; i < 256; i++) { + char c = (char)i; + + // Get serializers specifically triggered by this character + IEnumerable triggers = serializers.Where(s => s.TriggerCharacters.Contains(c)); + + // Result is Triggers + Globals + table[i] = triggers.Concat(globals).ToImmutableArray(); + } + + return table; + } +} diff --git a/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/MdStringMdSyntaxSerializer.cs b/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/MdStringMdSyntaxSerializer.cs index 51af350c..299b606e 100644 --- a/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/MdStringMdSyntaxSerializer.cs +++ b/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/MdStringMdSyntaxSerializer.cs @@ -1,68 +1,44 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- -using CodeOfChaos.Extensions.DependencyInjection; -using InfiniBlazor.Markdown.Parsers.Markdown.Serializer.NodeSerializers; -using InfiniBlazor.Markdown.Parsers.Markdown.Serializer.RegexLib; using InfiniBlazor.Markdown.Syntax; using Microsoft.Extensions.Logging; -using System.Collections.Frozen; +using System.Buffers; +using System.Collections.Immutable; using System.Text.RegularExpressions; namespace InfiniBlazor.Markdown.Parsers.Markdown.Serializer; // --------------------------------------------------------------------------------------------------------------------- // Code // --------------------------------------------------------------------------------------------------------------------- -[InjectableSingleton] public sealed class MdStringMdSyntaxSerializer(ILogger logger) : IMdStringMdSyntaxSerializer { - private delegate void MdSyntaxSerializerAction(IMdSyntaxFragmentStack stack, IMdSyntaxNode node, Match match); - - private readonly FrozenDictionary _elementHandlers = new Dictionary { - [MdRegexGroupNames.BlockQuote] = BlockQuoteSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.Bold] = BoldSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.Callout] = CalloutSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.CodeBlock] = CodeBlockSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.CodeInline] = CodeInlineSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.Emote] = EmoteSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.Escaped] = EscapedSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.HeadingSimple] = HeadingSimpleSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.Heading] = HeadingSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.HorizontalRule] = HorizontalRuleSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.HtmlBody] = HtmlSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.Italic] = ItalicSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.Link] = LinkSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.List] = ListSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.NewLine] = NewLineSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.Paragraph] = ParagraphSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.Strike] = StrikeSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.SubScript] = SubScriptSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.SuperScript] = SuperScriptSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.Table] = TableSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.Tag] = TagSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.Underline] = UnderlineSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.User] = UserSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.WikiLink] = WikiLinkSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.Template] = TemplateSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.FootnoteReference] = FootnoteReferenceSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.FootnoteDescription] = FootnoteDescriptionSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.Highlight] = HighlightSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.Wrapper] = WrapperSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.Frontmatter] = FrontmatterSyntaxNodeSerializer.Serialize, - [MdRegexGroupNames.Break] = BreakSyntaxNodeSerializer.Serialize, - }.ToFrozenDictionary(); - + public required ImmutableArray SingleLineSerializers { get; init; } + public required ImmutableArray[] SingleLineLookup { get; init; } + public required SearchValues SingleLineTriggerSearchValues { get; init; } + + public required ImmutableArray MultiLineSerializers { get; init; } + public required ImmutableArray[] MultiLineLookup { get; init; } + + public required IMdSyntaxNodeSerializer? FrontMatterSerializer { get; init; } + // ----------------------------------------------------------------------------------------------------------------- // Methods // ----------------------------------------------------------------------------------------------------------------- + public ImmutableArray GetSingleLineSerializersForChar(char c) + => c < 256 ? SingleLineLookup[c] : SingleLineSerializers; + + public ImmutableArray GetMultiLineSerializersForChar(char c) + => c < 256 ? MultiLineLookup[c] : MultiLineSerializers; + public IMdSyntaxTree SerializeToTree(string markdown) { - MdSyntaxTree nodeTree = MdSyntaxTree.Pool.Get(); + IMdSyntaxTree nodeTree = MdSyntaxTreePool.Shared.Get(); SerializeToTree(markdown, nodeTree); return nodeTree; } public void SerializeToTree(string markdown, IMdSyntaxTree nodeTree) { - MdSyntaxFragmentStack fragmentStack = MdSyntaxFragmentStack.Pool.Get(); - fragmentStack.TreeReference = nodeTree; + MdSyntaxFragmentStack fragmentStack = MdSyntaxFragmentStackPool.Shared.Get(); + fragmentStack.SerializerReference = this; string normalized = markdown.ReplaceLineEndings("\n"); @@ -73,54 +49,41 @@ public void SerializeToTree(string markdown, IMdSyntaxTree nodeTree) { while (fragmentStack.TryPopDto(out MdSyntaxFragment fragment)) { switch (fragment) { // Not yet processed - case { ParentNode: {} parentNode, Match: {} match }: { - ProcessMatch(match, parentNode, fragmentStack); + case { ParentNode: { } parentNode, Match: { } match, NodeSerializer: { } serializer }: { + serializer.Serialize(fragmentStack, parentNode, match); break; } // Already processed and simply needs to be added in correct location - case { ParentNode: {} parentNode, ChildNode: {} childNode }: { + case { ParentNode: { } parentNode, ChildNode: { } childNode }: { parentNode.AddChildNode(childNode); break; } // Unhandled state which should never happen default: { - logger.Error("Unhandled state in MarkdownMdSyntaxSerializer.SerializeToTree with fragment '{fragment}'.", fragment); + logger.LogError("Unhandled state in MdStringMdSyntaxSerializer.SerializeToTree with fragment '{Fragment}'.", fragment); break; } } } } catch (Exception ex) { - logger.Error(ex, "Error parsing Markdown, during tree conversion."); + logger.LogError(ex, "Error parsing Markdown during tree conversion."); throw; } finally { - MdSyntaxFragmentStack.Pool.Return(fragmentStack); + MdSyntaxFragmentStackPool.Shared.Return(fragmentStack); } } private void TryExtractFrontMatter(MdSyntaxFragmentStack fragmentStack, string markdown, IMdSyntaxTree nodeTree, out int newStartAtIndex) { newStartAtIndex = 0; - if (!_elementHandlers.TryGetValue(MdRegexGroupNames.Frontmatter, out MdSyntaxSerializerAction? handler)) return; - Match match = MdRegexLib.FindFrontmatterRegex.Match(markdown); + if (FrontMatterSerializer is null) return; + Match match = FrontMatterSerializer.Match(markdown); if (!match.Success) return; newStartAtIndex = match.Index + match.Length; - handler(fragmentStack, nodeTree.RootNode, match); - } - - private void ProcessMatch(Match match, IMdSyntaxNode parentNode, IMdSyntaxFragmentStack runningParser) { - GroupCollection groups = match.Groups; - int length = groups.Count; - if (length == 0) return; - - for (int index = 0; index < length; index++) { - Group group = groups[index]; - if (!group.Success || !_elementHandlers.TryGetValue(group.Name, out MdSyntaxSerializerAction? handler)) continue; - - handler(runningParser, parentNode, match); - } + FrontMatterSerializer.Serialize(fragmentStack, nodeTree.RootNode, match); } } diff --git a/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/MdSyntaxFragment.cs b/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/MdSyntaxFragment.cs index 4ebc67cd..458a9ffd 100644 --- a/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/MdSyntaxFragment.cs +++ b/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/MdSyntaxFragment.cs @@ -8,14 +8,14 @@ namespace InfiniBlazor.Markdown.Parsers.Markdown.Serializer; // --------------------------------------------------------------------------------------------------------------------- // Code // --------------------------------------------------------------------------------------------------------------------- -public readonly record struct MdSyntaxFragment(IMdSyntaxNode? ParentNode, IMdSyntaxNode? ChildNode, Match? Match) { +public readonly record struct MdSyntaxFragment(IMdSyntaxNode? ParentNode, IMdSyntaxNode? ChildNode, Match? Match, IMdSyntaxNodeSerializer? NodeSerializer) { // ----------------------------------------------------------------------------------------------------------------- // Constructors // ----------------------------------------------------------------------------------------------------------------- - public static MdSyntaxFragment AsUnhandledMatch(Match match, IMdSyntaxNode node) - => new(node, null, match); + public static MdSyntaxFragment AsUnhandledMatch(Match match, IMdSyntaxNode node, IMdSyntaxNodeSerializer nodeSerializer) + => new(node, null, match, nodeSerializer); public static MdSyntaxFragment AsProcessedNode(IMdSyntaxNode parentNode, IMdSyntaxNode childNode) - => new(parentNode, childNode, null); + => new(parentNode, childNode, null, null); } diff --git a/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/MdSyntaxFragmentStack.cs b/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/MdSyntaxFragmentStack.cs index c245f91f..373b0a50 100644 --- a/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/MdSyntaxFragmentStack.cs +++ b/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/MdSyntaxFragmentStack.cs @@ -1,11 +1,11 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- -using InfiniBlazor.Markdown.Parsers.Markdown.Serializer.RegexLib; using InfiniBlazor.Markdown.Syntax; using InfiniBlazor.Markdown.Syntax.Nodes; -using InfiniBlazor.Pooling; using Microsoft.Extensions.ObjectPool; +using System.Buffers; +using System.Collections.Immutable; using System.Text.RegularExpressions; namespace InfiniBlazor.Markdown.Parsers.Markdown.Serializer; @@ -13,62 +13,148 @@ namespace InfiniBlazor.Markdown.Parsers.Markdown.Serializer; // Code // --------------------------------------------------------------------------------------------------------------------- public sealed class MdSyntaxFragmentStack : IMdSyntaxFragmentStack, IResettable { - public IMdSyntaxTree TreeReference { get; set; } = null!; - + public IMdStringMdSyntaxSerializer SerializerReference { get; set; } = null!; + private readonly Stack _stack = new(); - public static ObjectPool Pool { get; } = PoolingHelpers.CreateResettablePool(PoolingHelpers.ParsersRetained); - // ----------------------------------------------------------------------------------------------------------------- // Methods // ----------------------------------------------------------------------------------------------------------------- #region PushToStack public void PushMultiLineMatchesToStack(string input, IMdSyntaxNode node, int startIndex = 0) { - MatchCollection matches = MdRegexLib.MultilineStructuresRegex.Matches(input, startIndex); - int count = matches.Count; - _stack.EnsureCapacity(_stack.Count + count); + int scanPos = startIndex; + int inputLength = input.Length; + int index = 0; + + MdSyntaxFragment[] fragments = ArrayPool.Shared.Rent(32); + + try { + while (scanPos < inputLength) { + char currentChar = input[scanPos]; + bool matched = false; + + // Get serializers that can trigger on this specific line-start character + ImmutableArray candidates = SerializerReference.GetMultiLineSerializersForChar(currentChar); + + foreach (IMdSyntaxNodeSerializer serializer in candidates) { + Match m = serializer.Match(input, scanPos); + if (!m.Success || m.Index != scanPos) continue; - for (int i = count - 1; i >= 0; i--) { - PushMatchToStack(matches[i], node); + EnsureCapacity(ref fragments, ref index, 1); + fragments[index++] = MdSyntaxFragment.AsUnhandledMatch(m, node, serializer); + + scanPos += Math.Max(1, m.Length); + matched = true; + break; + } + + if (matched) continue; + + // If no multiline block matched at this position, jump to the start of the next line. + // We don't need to check characters mid-line for block structures. + int nextLine = input.IndexOf('\n', scanPos); + if (nextLine == -1) break; + + scanPos = nextLine + 1; + } + + _stack.EnsureCapacity(_stack.Count + index); + for (int i = index - 1; i >= 0; i--) { + _stack.Push(fragments[i]); + } + } + finally { + ArrayPool.Shared.Return(fragments); } } public void PushSingleLineMatchesToStack(string input, IMdSyntaxNode node) { - MatchCollection matches = MdRegexLib.SinglelineStructuresRegex.Matches(input); - int count = matches.Count; - _stack.EnsureCapacity(_stack.Count + count); + int scanPos = 0; + int length = input.Length; + int textStart = 0; + int index = 0; + + SearchValues searchValues = SerializerReference.SingleLineTriggerSearchValues; + ReadOnlySpan span = input.AsSpan(); + + MdSyntaxFragment[] fragments = ArrayPool.Shared.Rent(128); + + try { + while (scanPos < length) { + // Find the next character that could potentially be a tag + int offset = span[scanPos..].IndexOfAny(searchValues); + if (offset == -1) break; // No more trigger characters left in the whole string + + // Move scanPos to the found trigger + scanPos += offset; + char currentChar = input[scanPos]; + + ImmutableArray candidates = SerializerReference.GetSingleLineSerializersForChar(currentChar); + IMdSyntaxNodeSerializer? winner = null; + Match? winningMatch = null; + + foreach (IMdSyntaxNodeSerializer serializer in candidates) { + Match m = serializer.Match(input, scanPos); + if (!m.Success || m.Index != scanPos) continue; - int currentIndex = input.Length; + winner = serializer; + winningMatch = m; + break; + } - for (int i = count - 1; i >= 0; i--) { - Match match = matches[i]; - int matchEnd = match.Index + match.Length; + if (winner != null && winningMatch != null) { + // Push everything from textStart up to scanPos as a TextNode + if (scanPos > textStart) { + TextMdSyntaxNode contentNode = MdSyntaxNodePool.Shared.Get(); + contentNode.WithContent(input[textStart..scanPos]); + EnsureCapacity(ref fragments, ref index, 1); + fragments[index++] = MdSyntaxFragment.AsProcessedNode(node, contentNode); + } - // If there's an uncaught text between this match's end and the last position, add it as raw input - if (matchEnd < currentIndex) { - TextMdSyntaxNode contentNode = TextMdSyntaxNode.Pool.Get(); - contentNode.WithContent(input[matchEnd..currentIndex]); - PushProcessedNodeToStack(node, contentNode); + // Push the actual Tag/Match + EnsureCapacity(ref fragments, ref index, 1); + fragments[index++] = MdSyntaxFragment.AsUnhandledMatch(winningMatch, node, winner); + + scanPos += Math.Max(1, winningMatch.Length); + textStart = scanPos; + } + else { + // It was a trigger character (like '*') but not a valid tag. + // Increment scanPos so IndexOfAny finds the NEXT trigger. + scanPos++; + } } - PushMatchToStack(match, node); - currentIndex = match.Index; - } + // Push any remaining trailing text + if (textStart < length) { + TextMdSyntaxNode tail = MdSyntaxNodePool.Shared.Get(); + tail.WithContent(input[textStart..]); + EnsureCapacity(ref fragments, ref index, 1); + fragments[index++] = MdSyntaxFragment.AsProcessedNode(node, tail); + } - // ReSharper disable once InvertIf - if (currentIndex > 0) { - // Handle any remaining text before the first match - TextMdSyntaxNode contentNode = TextMdSyntaxNode.Pool.Get(); - contentNode.WithContent(input[..currentIndex]); - PushProcessedNodeToStack(node, contentNode); + _stack.EnsureCapacity(_stack.Count + index); + for (int i = index - 1; i >= 0; i--) { + _stack.Push(fragments[i]); + } + } + finally { + ArrayPool.Shared.Return(fragments); } } + private static void EnsureCapacity(ref MdSyntaxFragment[] arr, ref int index, int required) { + if (index + required <= arr.Length) return; + + int newSize = arr.Length * 2; + MdSyntaxFragment[] newArr = ArrayPool.Shared.Rent(newSize); + Array.Copy(arr, newArr, index); + ArrayPool.Shared.Return(arr); + arr = newArr; + } + public void PushProcessedNodeToStack(IMdSyntaxNode parentNode, IMdSyntaxNode childNode) => _stack.Push(MdSyntaxFragment.AsProcessedNode(parentNode, childNode)); - - private void PushMatchToStack(Match match, IMdSyntaxNode currentNode) - => _stack.Push(MdSyntaxFragment.AsUnhandledMatch(match, currentNode)); #endregion public bool TryPopDto(out MdSyntaxFragment dto) { diff --git a/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/MdSyntaxFragmentStackPool.cs b/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/MdSyntaxFragmentStackPool.cs new file mode 100644 index 00000000..9f5a6a44 --- /dev/null +++ b/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/MdSyntaxFragmentStackPool.cs @@ -0,0 +1,22 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +using InfiniBlazor.Pooling; +using Microsoft.Extensions.ObjectPool; + +namespace InfiniBlazor.Markdown.Parsers.Markdown.Serializer; + +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +public class MdSyntaxFragmentStackPool { + public static MdSyntaxFragmentStackPool Shared { get; } = new(); + + private ObjectPool Pool { get; } = PoolingHelpers.CreateResettablePool(16); + + // ----------------------------------------------------------------------------------------------------------------- + // Methods + // ----------------------------------------------------------------------------------------------------------------- + public MdSyntaxFragmentStack Get() => Pool.Get(); + public void Return(MdSyntaxFragmentStack stack) => Pool.Return(stack); +} diff --git a/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/NodeSerializers/BlockQuoteSyntaxNodeSerializer.cs b/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/NodeSerializers/BlockQuoteSyntaxNodeSerializer.cs index cfadbcce..81d3dea5 100644 --- a/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/NodeSerializers/BlockQuoteSyntaxNodeSerializer.cs +++ b/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/NodeSerializers/BlockQuoteSyntaxNodeSerializer.cs @@ -1,7 +1,6 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- -using InfiniBlazor.Markdown.Parsers.Markdown.Serializer.RegexLib; using InfiniBlazor.Markdown.Syntax; using InfiniBlazor.Markdown.Syntax.Nodes; using System.Text.RegularExpressions; @@ -10,23 +9,26 @@ namespace InfiniBlazor.Markdown.Parsers.Markdown.Serializer.NodeSerializers; // --------------------------------------------------------------------------------------------------------------------- // Code // --------------------------------------------------------------------------------------------------------------------- -public static class BlockQuoteSyntaxNodeSerializer { - private static readonly int BlockQuoteId = MdRegexLib.GetGroupId(MdRegexGroupNames.BlockQuote); - +public partial class BlockQuoteSyntaxNodeSerializer : IMdSyntaxNodeSerializer { + [GeneratedRegex(@"\G^>[\ ]*(?:.+(?:\n>[^\n]*)*)$", RegexOptions.Multiline | RegexOptions.ExplicitCapture | RegexOptions.Compiled)] + private static partial Regex Syntax { get; } + + public char[] TriggerCharacters { get; } = ['>']; // ----------------------------------------------------------------------------------------------------------------- // Methods // ----------------------------------------------------------------------------------------------------------------- - public static void Serialize( + public Match Match(string input, int startPosition = 0) + => Syntax.Match(input, startPosition); + + public void Serialize( IMdSyntaxFragmentStack stack, IMdSyntaxNode parentNode, Match match ) { - Group group = match.Groups[BlockQuoteId]; - if (!group.TryGetValueSpan(out ReadOnlySpan blockQuoteBody)) return; - + ReadOnlySpan blockQuoteBody = match.ValueSpan; string adjustedBlockquote = LineNormalization.NormalizeBlockQuote(blockQuoteBody, out int leadingSpaces); - BlockQuoteMdSyntaxNode blockQuoteNode = BlockQuoteMdSyntaxNode.Pool.Get(); + BlockQuoteMdSyntaxNode blockQuoteNode = MdSyntaxNodePool.Shared.Get(); blockQuoteNode.WithLeadingSpaces(leadingSpaces); parentNode.AddChildNode(blockQuoteNode); diff --git a/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/NodeSerializers/BoldSyntaxNodeSerializer.cs b/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/NodeSerializers/BoldSyntaxNodeSerializer.cs index 352b7976..f8ce9a81 100644 --- a/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/NodeSerializers/BoldSyntaxNodeSerializer.cs +++ b/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/NodeSerializers/BoldSyntaxNodeSerializer.cs @@ -1,7 +1,6 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- -using InfiniBlazor.Markdown.Parsers.Markdown.Serializer.RegexLib; using InfiniBlazor.Markdown.Syntax; using InfiniBlazor.Markdown.Syntax.Nodes; using System.Text.RegularExpressions; @@ -10,16 +9,23 @@ namespace InfiniBlazor.Markdown.Parsers.Markdown.Serializer.NodeSerializers; // --------------------------------------------------------------------------------------------------------------------- // Code // --------------------------------------------------------------------------------------------------------------------- -public static class BoldSyntaxNodeSerializer { - private static readonly int BId = MdRegexLib.GetGroupId(MdRegexGroupNames.BoldContent); - +public partial class BoldSyntaxNodeSerializer : IMdSyntaxNodeSerializer { + [GeneratedRegex(@"\G\*\*(?(?>[^\\\*]+|\\\*|\*|(?\*\*)|(?<-open>\*\*))+)(?(open)(?!))\*\*", RegexOptions.ExplicitCapture | RegexOptions.Compiled)] + private static partial Regex Syntax { get; } + + public char[] TriggerCharacters { get; } = ['*']; + + private static readonly int BoldContentId = Syntax.GroupNumberFromName("b"); // ----------------------------------------------------------------------------------------------------------------- // Methods // ----------------------------------------------------------------------------------------------------------------- - public static void Serialize(IMdSyntaxFragmentStack stack, IMdSyntaxNode parentNode, Match match) { - if (!match.Groups[BId].TryGetValue(out string? boldValue)) return; + public Match Match(string input, int startPosition = 0) + => Syntax.Match(input, startPosition); + + public void Serialize(IMdSyntaxFragmentStack stack, IMdSyntaxNode parentNode, Match match) { + string boldValue = match.Groups[BoldContentId].Value; - BoldMdSyntaxNode node = BoldMdSyntaxNode.Pool.Get(); + BoldMdSyntaxNode node = MdSyntaxNodePool.Shared.Get(); parentNode.AddChildNode(node); stack.PushSingleLineMatchesToStack(boldValue, node); } diff --git a/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/NodeSerializers/BreakSyntaxNodeSerializer.cs b/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/NodeSerializers/BreakSyntaxNodeSerializer.cs index 6550f254..404955ad 100644 --- a/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/NodeSerializers/BreakSyntaxNodeSerializer.cs +++ b/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/NodeSerializers/BreakSyntaxNodeSerializer.cs @@ -9,12 +9,19 @@ namespace InfiniBlazor.Markdown.Parsers.Markdown.Serializer.NodeSerializers; // --------------------------------------------------------------------------------------------------------------------- // Code // --------------------------------------------------------------------------------------------------------------------- -public static class BreakSyntaxNodeSerializer { +public partial class BreakSyntaxNodeSerializer : IMdSyntaxNodeSerializer { + [GeneratedRegex(@"\G<[Bb][Rr]/?>", RegexOptions.ExplicitCapture | RegexOptions.Compiled)] + private static partial Regex Syntax { get; } + + public char[] TriggerCharacters { get; } = ['<']; // ----------------------------------------------------------------------------------------------------------------- // Methods // ----------------------------------------------------------------------------------------------------------------- - public static void Serialize(IMdSyntaxFragmentStack stack, IMdSyntaxNode parentNode, Match match) { - BreakMdSyntaxNode node = BreakMdSyntaxNode.Pool.Get(); + public Match Match(string input, int startPosition = 0) + => Syntax.Match(input, startPosition); + + public void Serialize(IMdSyntaxFragmentStack stack, IMdSyntaxNode parentNode, Match match) { + BreakMdSyntaxNode node = MdSyntaxNodePool.Shared.Get(); parentNode.AddChildNode(node); } } diff --git a/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/NodeSerializers/CalloutSyntaxNodeSerializer.cs b/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/NodeSerializers/CalloutSyntaxNodeSerializer.cs index 9b752cad..a2791143 100644 --- a/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/NodeSerializers/CalloutSyntaxNodeSerializer.cs +++ b/src/InfiniBlazor.Core.Markdown/Parsers/Markdown/Serializer/NodeSerializers/CalloutSyntaxNodeSerializer.cs @@ -1,7 +1,6 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- -using InfiniBlazor.Markdown.Parsers.Markdown.Serializer.RegexLib; using InfiniBlazor.Markdown.Syntax; using InfiniBlazor.Markdown.Syntax.Nodes; using System.Text.RegularExpressions; @@ -10,22 +9,33 @@ namespace InfiniBlazor.Markdown.Parsers.Markdown.Serializer.NodeSerializers; // --------------------------------------------------------------------------------------------------------------------- // Code // --------------------------------------------------------------------------------------------------------------------- -public static class CalloutSyntaxNodeSerializer { - private static readonly int CalloutTypeId = MdRegexLib.GetGroupId(MdRegexGroupNames.CalloutType); - private static readonly int CalloutModId = MdRegexLib.GetGroupId(MdRegexGroupNames.CalloutMod); - private static readonly int CalloutTitleId = MdRegexLib.GetGroupId(MdRegexGroupNames.CalloutTitle); - private static readonly int CalloutBodyId = MdRegexLib.GetGroupId(MdRegexGroupNames.CalloutBody); - private static readonly int CalloutOptionId = MdRegexLib.GetGroupId(MdRegexGroupNames.CalloutOption); +public partial class CalloutSyntaxNodeSerializer : IMdSyntaxNodeSerializer { + [GeneratedRegex(""" + \G + ^>(?:\[!(?[^\|\n]+)(?\|[^\n]*)?\](?