From b672c30071461a600fb546993a3f613f719c6793 Mon Sep 17 00:00:00 2001 From: Oleksii Holub <1935960+Tyrrrz@users.noreply.github.com> Date: Thu, 3 Nov 2022 19:13:50 +0200 Subject: [PATCH] Pass cancellation token to markdown visitor --- .../Exporting/Writers/CsvMessageWriter.cs | 8 +- .../Writers/Html/MessageGroupTemplate.cshtml | 4 +- .../Writers/Html/PreambleTemplate.cshtml | 2 +- .../Exporting/Writers/JsonMessageWriter.cs | 16 ++-- .../MarkdownVisitors/HtmlMarkdownVisitor.cs | 57 ++++++++----- .../PlainTextMarkdownVisitor.cs | 33 ++++--- .../Writers/PlainTextMessageWriter.cs | 54 ++++++++++-- .../Markdown/Parsing/MarkdownVisitor.cs | 85 +++++++++++++------ 8 files changed, 185 insertions(+), 74 deletions(-) diff --git a/DiscordChatExporter.Core/Exporting/Writers/CsvMessageWriter.cs b/DiscordChatExporter.Core/Exporting/Writers/CsvMessageWriter.cs index 214ba1b7..c91c0875 100644 --- a/DiscordChatExporter.Core/Exporting/Writers/CsvMessageWriter.cs +++ b/DiscordChatExporter.Core/Exporting/Writers/CsvMessageWriter.cs @@ -19,8 +19,10 @@ internal partial class CsvMessageWriter : MessageWriter _writer = new StreamWriter(stream); } - private ValueTask FormatMarkdownAsync(string? markdown) => - PlainTextMarkdownVisitor.FormatAsync(Context, markdown ?? ""); + private ValueTask FormatMarkdownAsync( + string markdown, + CancellationToken cancellationToken = default) => + PlainTextMarkdownVisitor.FormatAsync(Context, markdown, cancellationToken); public override async ValueTask WritePreambleAsync(CancellationToken cancellationToken = default) => await _writer.WriteLineAsync("AuthorID,Author,Date,Content,Attachments,Reactions"); @@ -84,7 +86,7 @@ internal partial class CsvMessageWriter : MessageWriter await _writer.WriteAsync(','); // Message content - await _writer.WriteAsync(CsvEncode(await FormatMarkdownAsync(message.Content))); + await _writer.WriteAsync(CsvEncode(await FormatMarkdownAsync(message.Content, cancellationToken))); await _writer.WriteAsync(','); // Attachments diff --git a/DiscordChatExporter.Core/Exporting/Writers/Html/MessageGroupTemplate.cshtml b/DiscordChatExporter.Core/Exporting/Writers/Html/MessageGroupTemplate.cshtml index f0d0555f..e453a7b6 100644 --- a/DiscordChatExporter.Core/Exporting/Writers/Html/MessageGroupTemplate.cshtml +++ b/DiscordChatExporter.Core/Exporting/Writers/Html/MessageGroupTemplate.cshtml @@ -30,10 +30,10 @@ ExportContext.FormatDate(date); ValueTask FormatMarkdownAsync(string markdown) => - HtmlMarkdownVisitor.FormatAsync(ExportContext, markdown); + HtmlMarkdownVisitor.FormatAsync(ExportContext, markdown, true, CancellationToken); ValueTask FormatEmbedMarkdownAsync(string markdown) => - HtmlMarkdownVisitor.FormatAsync(ExportContext, markdown, false); + HtmlMarkdownVisitor.FormatAsync(ExportContext, markdown, false, CancellationToken); var firstMessage = Messages.First(); diff --git a/DiscordChatExporter.Core/Exporting/Writers/Html/PreambleTemplate.cshtml b/DiscordChatExporter.Core/Exporting/Writers/Html/PreambleTemplate.cshtml index 4ac127ce..ec61d2e0 100644 --- a/DiscordChatExporter.Core/Exporting/Writers/Html/PreambleTemplate.cshtml +++ b/DiscordChatExporter.Core/Exporting/Writers/Html/PreambleTemplate.cshtml @@ -32,7 +32,7 @@ ExportContext.FormatDate(date); ValueTask FormatMarkdownAsync(string markdown) => - HtmlMarkdownVisitor.FormatAsync(ExportContext, markdown); + HtmlMarkdownVisitor.FormatAsync(ExportContext, markdown, true, CancellationToken); } diff --git a/DiscordChatExporter.Core/Exporting/Writers/JsonMessageWriter.cs b/DiscordChatExporter.Core/Exporting/Writers/JsonMessageWriter.cs index 4fa49cab..9d9a1253 100644 --- a/DiscordChatExporter.Core/Exporting/Writers/JsonMessageWriter.cs +++ b/DiscordChatExporter.Core/Exporting/Writers/JsonMessageWriter.cs @@ -29,8 +29,10 @@ internal class JsonMessageWriter : MessageWriter }); } - private ValueTask FormatMarkdownAsync(string? markdown) => - PlainTextMarkdownVisitor.FormatAsync(Context, markdown ?? ""); + private ValueTask FormatMarkdownAsync( + string markdown, + CancellationToken cancellationToken = default) => + PlainTextMarkdownVisitor.FormatAsync(Context, markdown, cancellationToken); private async ValueTask WriteAttachmentAsync( Attachment attachment, @@ -115,8 +117,8 @@ internal class JsonMessageWriter : MessageWriter { _writer.WriteStartObject(); - _writer.WriteString("name", await FormatMarkdownAsync(embedField.Name)); - _writer.WriteString("value", await FormatMarkdownAsync(embedField.Value)); + _writer.WriteString("name", await FormatMarkdownAsync(embedField.Name, cancellationToken)); + _writer.WriteString("value", await FormatMarkdownAsync(embedField.Value, cancellationToken)); _writer.WriteBoolean("isInline", embedField.IsInline); _writer.WriteEndObject(); @@ -129,10 +131,10 @@ internal class JsonMessageWriter : MessageWriter { _writer.WriteStartObject(); - _writer.WriteString("title", await FormatMarkdownAsync(embed.Title)); + _writer.WriteString("title", await FormatMarkdownAsync(embed.Title ?? "", cancellationToken)); _writer.WriteString("url", embed.Url); _writer.WriteString("timestamp", embed.Timestamp); - _writer.WriteString("description", await FormatMarkdownAsync(embed.Description)); + _writer.WriteString("description", await FormatMarkdownAsync(embed.Description ?? "", cancellationToken)); if (embed.Color is not null) _writer.WriteString("color", embed.Color.Value.ToHex()); @@ -283,7 +285,7 @@ internal class JsonMessageWriter : MessageWriter _writer.WriteBoolean("isPinned", message.IsPinned); // Content - _writer.WriteString("content", await FormatMarkdownAsync(message.Content)); + _writer.WriteString("content", await FormatMarkdownAsync(message.Content, cancellationToken)); // Author _writer.WriteStartObject("author"); diff --git a/DiscordChatExporter.Core/Exporting/Writers/MarkdownVisitors/HtmlMarkdownVisitor.cs b/DiscordChatExporter.Core/Exporting/Writers/MarkdownVisitors/HtmlMarkdownVisitor.cs index e7117cc1..919c3c6d 100644 --- a/DiscordChatExporter.Core/Exporting/Writers/MarkdownVisitors/HtmlMarkdownVisitor.cs +++ b/DiscordChatExporter.Core/Exporting/Writers/MarkdownVisitors/HtmlMarkdownVisitor.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Net; using System.Text; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using DiscordChatExporter.Core.Discord.Data; using DiscordChatExporter.Core.Markdown; @@ -24,13 +25,17 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor _isJumbo = isJumbo; } - protected override async ValueTask VisitTextAsync(TextNode text) + protected override async ValueTask VisitTextAsync( + TextNode text, + CancellationToken cancellationToken = default) { _buffer.Append(HtmlEncode(text.Text)); - return await base.VisitTextAsync(text); + return await base.VisitTextAsync(text, cancellationToken); } - protected override async ValueTask VisitFormattingAsync(FormattingNode formatting) + protected override async ValueTask VisitFormattingAsync( + FormattingNode formatting, + CancellationToken cancellationToken = default) { var (openingTag, closingTag) = formatting.Kind switch { @@ -68,23 +73,27 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor }; _buffer.Append(openingTag); - var result = await base.VisitFormattingAsync(formatting); + var result = await base.VisitFormattingAsync(formatting, cancellationToken); _buffer.Append(closingTag); return result; } - protected override async ValueTask VisitInlineCodeBlockAsync(InlineCodeBlockNode inlineCodeBlock) + protected override async ValueTask VisitInlineCodeBlockAsync( + InlineCodeBlockNode inlineCodeBlock, + CancellationToken cancellationToken = default) { _buffer .Append("") .Append(HtmlEncode(inlineCodeBlock.Code)) .Append(""); - return await base.VisitInlineCodeBlockAsync(inlineCodeBlock); + return await base.VisitInlineCodeBlockAsync(inlineCodeBlock, cancellationToken); } - protected override async ValueTask VisitMultiLineCodeBlockAsync(MultiLineCodeBlockNode multiLineCodeBlock) + protected override async ValueTask VisitMultiLineCodeBlockAsync( + MultiLineCodeBlockNode multiLineCodeBlock, + CancellationToken cancellationToken = default) { var highlightCssClass = !string.IsNullOrWhiteSpace(multiLineCodeBlock.Language) ? $"language-{multiLineCodeBlock.Language}" @@ -95,10 +104,12 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor .Append(HtmlEncode(multiLineCodeBlock.Code)) .Append(""); - return await base.VisitMultiLineCodeBlockAsync(multiLineCodeBlock); + return await base.VisitMultiLineCodeBlockAsync(multiLineCodeBlock, cancellationToken); } - protected override async ValueTask VisitLinkAsync(LinkNode link) + protected override async ValueTask VisitLinkAsync( + LinkNode link, + CancellationToken cancellationToken = default) { // Try to extract message ID if the link refers to a Discord message var linkedMessageId = Regex.Match( @@ -112,13 +123,15 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor : $"" ); - var result = await base.VisitLinkAsync(link); + var result = await base.VisitLinkAsync(link, cancellationToken); _buffer.Append(""); return result; } - protected override async ValueTask VisitEmojiAsync(EmojiNode emoji) + protected override async ValueTask VisitEmojiAsync( + EmojiNode emoji, + CancellationToken cancellationToken = default) { var emojiImageUrl = Emoji.GetImageUrl(emoji.Id, emoji.Name, emoji.IsAnimated); var jumboClass = _isJumbo ? "chatlog__emoji--large" : ""; @@ -129,14 +142,16 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor $"class=\"chatlog__emoji {jumboClass}\" " + $"alt=\"{emoji.Name}\" " + $"title=\"{emoji.Code}\" " + - $"src=\"{await _context.ResolveAssetUrlAsync(emojiImageUrl)}\"" + + $"src=\"{await _context.ResolveAssetUrlAsync(emojiImageUrl, cancellationToken)}\"" + $">" ); - return await base.VisitEmojiAsync(emoji); + return await base.VisitEmojiAsync(emoji, cancellationToken); } - protected override async ValueTask VisitMentionAsync(MentionNode mention) + protected override async ValueTask VisitMentionAsync( + MentionNode mention, + CancellationToken cancellationToken = default) { if (mention.Kind == MentionKind.Everyone) { @@ -191,10 +206,12 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor .Append(""); } - return await base.VisitMentionAsync(mention); + return await base.VisitMentionAsync(mention, cancellationToken); } - protected override async ValueTask VisitUnixTimestampAsync(UnixTimestampNode timestamp) + protected override async ValueTask VisitUnixTimestampAsync( + UnixTimestampNode timestamp, + CancellationToken cancellationToken = default) { var dateString = timestamp.Date is not null ? _context.FormatDate(timestamp.Date.Value) @@ -210,7 +227,7 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor .Append(HtmlEncode(dateString)) .Append(""); - return await base.VisitUnixTimestampAsync(timestamp); + return await base.VisitUnixTimestampAsync(timestamp, cancellationToken); } } @@ -221,7 +238,8 @@ internal partial class HtmlMarkdownVisitor public static async ValueTask FormatAsync( ExportContext context, string markdown, - bool isJumboAllowed = true) + bool isJumboAllowed = true, + CancellationToken cancellationToken = default) { var nodes = MarkdownParser.Parse(markdown); @@ -231,7 +249,8 @@ internal partial class HtmlMarkdownVisitor var buffer = new StringBuilder(); - await new HtmlMarkdownVisitor(context, buffer, isJumbo).VisitAsync(nodes); + await new HtmlMarkdownVisitor(context, buffer, isJumbo) + .VisitAsync(nodes, cancellationToken); return buffer.ToString(); } diff --git a/DiscordChatExporter.Core/Exporting/Writers/MarkdownVisitors/PlainTextMarkdownVisitor.cs b/DiscordChatExporter.Core/Exporting/Writers/MarkdownVisitors/PlainTextMarkdownVisitor.cs index dffdc927..5224470e 100644 --- a/DiscordChatExporter.Core/Exporting/Writers/MarkdownVisitors/PlainTextMarkdownVisitor.cs +++ b/DiscordChatExporter.Core/Exporting/Writers/MarkdownVisitors/PlainTextMarkdownVisitor.cs @@ -1,4 +1,5 @@ using System.Text; +using System.Threading; using System.Threading.Tasks; using DiscordChatExporter.Core.Markdown; using DiscordChatExporter.Core.Markdown.Parsing; @@ -17,13 +18,17 @@ internal partial class PlainTextMarkdownVisitor : MarkdownVisitor _buffer = buffer; } - protected override async ValueTask VisitTextAsync(TextNode text) + protected override async ValueTask VisitTextAsync( + TextNode text, + CancellationToken cancellationToken = default) { _buffer.Append(text.Text); - return await base.VisitTextAsync(text); + return await base.VisitTextAsync(text, cancellationToken); } - protected override async ValueTask VisitEmojiAsync(EmojiNode emoji) + protected override async ValueTask VisitEmojiAsync( + EmojiNode emoji, + CancellationToken cancellationToken = default) { _buffer.Append( emoji.IsCustomEmoji @@ -31,10 +36,12 @@ internal partial class PlainTextMarkdownVisitor : MarkdownVisitor : emoji.Name ); - return await base.VisitEmojiAsync(emoji); + return await base.VisitEmojiAsync(emoji, cancellationToken); } - protected override async ValueTask VisitMentionAsync(MentionNode mention) + protected override async ValueTask VisitMentionAsync( + MentionNode mention, + CancellationToken cancellationToken = default) { if (mention.Kind == MentionKind.Everyone) { @@ -70,10 +77,12 @@ internal partial class PlainTextMarkdownVisitor : MarkdownVisitor _buffer.Append($"@{name}"); } - return await base.VisitMentionAsync(mention); + return await base.VisitMentionAsync(mention, cancellationToken); } - protected override async ValueTask VisitUnixTimestampAsync(UnixTimestampNode timestamp) + protected override async ValueTask VisitUnixTimestampAsync( + UnixTimestampNode timestamp, + CancellationToken cancellationToken = default) { _buffer.Append( timestamp.Date is not null @@ -81,18 +90,22 @@ internal partial class PlainTextMarkdownVisitor : MarkdownVisitor : "Invalid date" ); - return await base.VisitUnixTimestampAsync(timestamp); + return await base.VisitUnixTimestampAsync(timestamp, cancellationToken); } } internal partial class PlainTextMarkdownVisitor { - public static async ValueTask FormatAsync(ExportContext context, string markdown) + public static async ValueTask FormatAsync( + ExportContext context, + string markdown, + CancellationToken cancellationToken = default) { var nodes = MarkdownParser.ParseMinimal(markdown); var buffer = new StringBuilder(); - await new PlainTextMarkdownVisitor(context, buffer).VisitAsync(nodes); + await new PlainTextMarkdownVisitor(context, buffer) + .VisitAsync(nodes, cancellationToken); return buffer.ToString(); } diff --git a/DiscordChatExporter.Core/Exporting/Writers/PlainTextMessageWriter.cs b/DiscordChatExporter.Core/Exporting/Writers/PlainTextMessageWriter.cs index 17252637..4e97beca 100644 --- a/DiscordChatExporter.Core/Exporting/Writers/PlainTextMessageWriter.cs +++ b/DiscordChatExporter.Core/Exporting/Writers/PlainTextMessageWriter.cs @@ -19,8 +19,10 @@ internal class PlainTextMessageWriter : MessageWriter _writer = new StreamWriter(stream); } - private ValueTask FormatMarkdownAsync(string? markdown) => - PlainTextMarkdownVisitor.FormatAsync(Context, markdown ?? ""); + private ValueTask FormatMarkdownAsync( + string markdown, + CancellationToken cancellationToken = default) => + PlainTextMarkdownVisitor.FormatAsync(Context, markdown, cancellationToken); private async ValueTask WriteMessageHeaderAsync(Message message) { @@ -48,7 +50,9 @@ internal class PlainTextMessageWriter : MessageWriter { cancellationToken.ThrowIfCancellationRequested(); - await _writer.WriteLineAsync(await Context.ResolveAssetUrlAsync(attachment.Url, cancellationToken)); + await _writer.WriteLineAsync( + await Context.ResolveAssetUrlAsync(attachment.Url, cancellationToken) + ); } await _writer.WriteLineAsync(); @@ -65,24 +69,44 @@ internal class PlainTextMessageWriter : MessageWriter await _writer.WriteLineAsync("{Embed}"); if (!string.IsNullOrWhiteSpace(embed.Author?.Name)) + { await _writer.WriteLineAsync(embed.Author.Name); + } if (!string.IsNullOrWhiteSpace(embed.Url)) + { await _writer.WriteLineAsync(embed.Url); + } if (!string.IsNullOrWhiteSpace(embed.Title)) - await _writer.WriteLineAsync(await FormatMarkdownAsync(embed.Title)); + { + await _writer.WriteLineAsync( + await FormatMarkdownAsync(embed.Title, cancellationToken) + ); + } if (!string.IsNullOrWhiteSpace(embed.Description)) - await _writer.WriteLineAsync(await FormatMarkdownAsync(embed.Description)); + { + await _writer.WriteLineAsync( + await FormatMarkdownAsync(embed.Description, cancellationToken) + ); + } foreach (var field in embed.Fields) { if (!string.IsNullOrWhiteSpace(field.Name)) - await _writer.WriteLineAsync(await FormatMarkdownAsync(field.Name)); + { + await _writer.WriteLineAsync( + await FormatMarkdownAsync(field.Name, cancellationToken) + ); + } if (!string.IsNullOrWhiteSpace(field.Value)) - await _writer.WriteLineAsync(await FormatMarkdownAsync(field.Value)); + { + await _writer.WriteLineAsync( + await FormatMarkdownAsync(field.Value, cancellationToken) + ); + } } if (!string.IsNullOrWhiteSpace(embed.Thumbnail?.Url)) @@ -109,7 +133,9 @@ internal class PlainTextMessageWriter : MessageWriter } if (!string.IsNullOrWhiteSpace(embed.Footer?.Text)) + { await _writer.WriteLineAsync(embed.Footer.Text); + } await _writer.WriteLineAsync(); } @@ -152,7 +178,9 @@ internal class PlainTextMessageWriter : MessageWriter await _writer.WriteAsync(reaction.Emoji.Name); if (reaction.Count > 1) + { await _writer.WriteAsync($" ({reaction.Count})"); + } await _writer.WriteAsync(' '); } @@ -167,13 +195,19 @@ internal class PlainTextMessageWriter : MessageWriter await _writer.WriteLineAsync($"Channel: {Context.Request.Channel.Category.Name} / {Context.Request.Channel.Name}"); if (!string.IsNullOrWhiteSpace(Context.Request.Channel.Topic)) + { await _writer.WriteLineAsync($"Topic: {Context.Request.Channel.Topic}"); + } if (Context.Request.After is not null) + { await _writer.WriteLineAsync($"After: {Context.FormatDate(Context.Request.After.Value.ToDate())}"); + } if (Context.Request.Before is not null) + { await _writer.WriteLineAsync($"Before: {Context.FormatDate(Context.Request.Before.Value.ToDate())}"); + } await _writer.WriteLineAsync(new string('=', 62)); await _writer.WriteLineAsync(); @@ -190,7 +224,11 @@ internal class PlainTextMessageWriter : MessageWriter // Content if (!string.IsNullOrWhiteSpace(message.Content)) - await _writer.WriteLineAsync(await FormatMarkdownAsync(message.Content)); + { + await _writer.WriteLineAsync( + await FormatMarkdownAsync(message.Content, cancellationToken) + ); + } await _writer.WriteLineAsync(); diff --git a/DiscordChatExporter.Core/Markdown/Parsing/MarkdownVisitor.cs b/DiscordChatExporter.Core/Markdown/Parsing/MarkdownVisitor.cs index 325152b2..0da5e6a7 100644 --- a/DiscordChatExporter.Core/Markdown/Parsing/MarkdownVisitor.cs +++ b/DiscordChatExporter.Core/Markdown/Parsing/MarkdownVisitor.cs @@ -1,57 +1,94 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace DiscordChatExporter.Core.Markdown.Parsing; internal abstract class MarkdownVisitor { - protected virtual ValueTask VisitTextAsync(TextNode text) => + protected virtual ValueTask VisitTextAsync( + TextNode text, + CancellationToken cancellationToken = default) => new(text); - protected virtual async ValueTask VisitFormattingAsync(FormattingNode formatting) + protected virtual async ValueTask VisitFormattingAsync( + FormattingNode formatting, + CancellationToken cancellationToken = default) { - await VisitAsync(formatting.Children); + await VisitAsync(formatting.Children, cancellationToken); return formatting; } - protected virtual ValueTask VisitInlineCodeBlockAsync(InlineCodeBlockNode inlineCodeBlock) => + protected virtual ValueTask VisitInlineCodeBlockAsync( + InlineCodeBlockNode inlineCodeBlock, + CancellationToken cancellationToken = default) => new(inlineCodeBlock); - protected virtual ValueTask VisitMultiLineCodeBlockAsync(MultiLineCodeBlockNode multiLineCodeBlock) => + protected virtual ValueTask VisitMultiLineCodeBlockAsync( + MultiLineCodeBlockNode multiLineCodeBlock, + CancellationToken cancellationToken = default) => new(multiLineCodeBlock); - protected virtual async ValueTask VisitLinkAsync(LinkNode link) + protected virtual async ValueTask VisitLinkAsync( + LinkNode link, + CancellationToken cancellationToken = default) { - await VisitAsync(link.Children); + await VisitAsync(link.Children, cancellationToken); return link; } - protected virtual ValueTask VisitEmojiAsync(EmojiNode emoji) => + protected virtual ValueTask VisitEmojiAsync( + EmojiNode emoji, + CancellationToken cancellationToken = default) => new(emoji); - protected virtual ValueTask VisitMentionAsync(MentionNode mention) => + protected virtual ValueTask VisitMentionAsync( + MentionNode mention, + CancellationToken cancellationToken = default) => new(mention); - protected virtual ValueTask VisitUnixTimestampAsync(UnixTimestampNode timestamp) => + protected virtual ValueTask VisitUnixTimestampAsync( + UnixTimestampNode timestamp, + CancellationToken cancellationToken = default) => new(timestamp); - public async ValueTask VisitAsync(MarkdownNode node) => node switch - { - TextNode text => await VisitTextAsync(text), - FormattingNode formatting => await VisitFormattingAsync(formatting), - InlineCodeBlockNode inlineCodeBlock => await VisitInlineCodeBlockAsync(inlineCodeBlock), - MultiLineCodeBlockNode multiLineCodeBlock => await VisitMultiLineCodeBlockAsync(multiLineCodeBlock), - LinkNode link => await VisitLinkAsync(link), - EmojiNode emoji => await VisitEmojiAsync(emoji), - MentionNode mention => await VisitMentionAsync(mention), - UnixTimestampNode timestamp => await VisitUnixTimestampAsync(timestamp), - _ => throw new ArgumentOutOfRangeException(nameof(node)) - }; + public async ValueTask VisitAsync( + MarkdownNode node, + CancellationToken cancellationToken = default) => node switch + { + TextNode text => + await VisitTextAsync(text, cancellationToken), - public async ValueTask VisitAsync(IEnumerable nodes) + FormattingNode formatting => + await VisitFormattingAsync(formatting, cancellationToken), + + InlineCodeBlockNode inlineCodeBlock => + await VisitInlineCodeBlockAsync(inlineCodeBlock, cancellationToken), + + MultiLineCodeBlockNode multiLineCodeBlock => + await VisitMultiLineCodeBlockAsync(multiLineCodeBlock, cancellationToken), + + LinkNode link => + await VisitLinkAsync(link, cancellationToken), + + EmojiNode emoji => + await VisitEmojiAsync(emoji, cancellationToken), + + MentionNode mention => + await VisitMentionAsync(mention, cancellationToken), + + UnixTimestampNode timestamp => + await VisitUnixTimestampAsync(timestamp, cancellationToken), + + _ => throw new ArgumentOutOfRangeException(nameof(node)) + }; + + public async ValueTask VisitAsync( + IEnumerable nodes, + CancellationToken cancellationToken = default) { foreach (var node in nodes) - await VisitAsync(node); + await VisitAsync(node, cancellationToken); } } \ No newline at end of file