|
|
@ -16,302 +16,354 @@ namespace DiscordChatExporter.Core.Markdown.Parsing;
|
|
|
|
internal static partial class MarkdownParser
|
|
|
|
internal static partial class MarkdownParser
|
|
|
|
{
|
|
|
|
{
|
|
|
|
private const RegexOptions DefaultRegexOptions =
|
|
|
|
private const RegexOptions DefaultRegexOptions =
|
|
|
|
RegexOptions.Compiled |
|
|
|
|
RegexOptions.Compiled
|
|
|
|
RegexOptions.IgnorePatternWhitespace |
|
|
|
|
| RegexOptions.IgnorePatternWhitespace
|
|
|
|
RegexOptions.CultureInvariant |
|
|
|
|
| RegexOptions.CultureInvariant
|
|
|
|
RegexOptions.Multiline;
|
|
|
|
| RegexOptions.Multiline;
|
|
|
|
|
|
|
|
|
|
|
|
/* Formatting */
|
|
|
|
/* Formatting */
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> BoldFormattingNodeMatcher = new RegexMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> BoldFormattingNodeMatcher =
|
|
|
|
// There must be exactly two closing asterisks.
|
|
|
|
new RegexMatcher<MarkdownNode>(
|
|
|
|
new Regex(@"\*\*(.+?)\*\*(?!\*)", DefaultRegexOptions | RegexOptions.Singleline),
|
|
|
|
// There must be exactly two closing asterisks.
|
|
|
|
(s, m) => new FormattingNode(FormattingKind.Bold, Parse(s.Relocate(m.Groups[1])))
|
|
|
|
new Regex(@"\*\*(.+?)\*\*(?!\*)", DefaultRegexOptions | RegexOptions.Singleline),
|
|
|
|
);
|
|
|
|
(s, m) => new FormattingNode(FormattingKind.Bold, Parse(s.Relocate(m.Groups[1])))
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> ItalicFormattingNodeMatcher = new RegexMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> ItalicFormattingNodeMatcher =
|
|
|
|
// There must be exactly one closing asterisk.
|
|
|
|
new RegexMatcher<MarkdownNode>(
|
|
|
|
// Opening asterisk must not be followed by whitespace.
|
|
|
|
// There must be exactly one closing asterisk.
|
|
|
|
// Closing asterisk must not be preceded by whitespace.
|
|
|
|
// Opening asterisk must not be followed by whitespace.
|
|
|
|
new Regex(@"\*(?!\s)(.+?)(?<!\s|\*)\*(?!\*)", DefaultRegexOptions | RegexOptions.Singleline),
|
|
|
|
// Closing asterisk must not be preceded by whitespace.
|
|
|
|
(s, m) => new FormattingNode(FormattingKind.Italic, Parse(s.Relocate(m.Groups[1])))
|
|
|
|
new Regex(
|
|
|
|
);
|
|
|
|
@"\*(?!\s)(.+?)(?<!\s|\*)\*(?!\*)",
|
|
|
|
|
|
|
|
DefaultRegexOptions | RegexOptions.Singleline
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
(s, m) => new FormattingNode(FormattingKind.Italic, Parse(s.Relocate(m.Groups[1])))
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> ItalicBoldFormattingNodeMatcher = new RegexMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> ItalicBoldFormattingNodeMatcher =
|
|
|
|
// There must be exactly three closing asterisks.
|
|
|
|
new RegexMatcher<MarkdownNode>(
|
|
|
|
new Regex(@"\*(\*\*.+?\*\*)\*(?!\*)", DefaultRegexOptions | RegexOptions.Singleline),
|
|
|
|
// There must be exactly three closing asterisks.
|
|
|
|
(s, m) => new FormattingNode(FormattingKind.Italic, Parse(s.Relocate(m.Groups[1]), BoldFormattingNodeMatcher))
|
|
|
|
new Regex(@"\*(\*\*.+?\*\*)\*(?!\*)", DefaultRegexOptions | RegexOptions.Singleline),
|
|
|
|
);
|
|
|
|
(s, m) =>
|
|
|
|
|
|
|
|
new FormattingNode(
|
|
|
|
|
|
|
|
FormattingKind.Italic,
|
|
|
|
|
|
|
|
Parse(s.Relocate(m.Groups[1]), BoldFormattingNodeMatcher)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> ItalicAltFormattingNodeMatcher = new RegexMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> ItalicAltFormattingNodeMatcher =
|
|
|
|
// Closing underscore must not be followed by a word character.
|
|
|
|
new RegexMatcher<MarkdownNode>(
|
|
|
|
new Regex(@"_(.+?)_(?!\w)", DefaultRegexOptions | RegexOptions.Singleline),
|
|
|
|
// Closing underscore must not be followed by a word character.
|
|
|
|
(s, m) => new FormattingNode(FormattingKind.Italic, Parse(s.Relocate(m.Groups[1])))
|
|
|
|
new Regex(@"_(.+?)_(?!\w)", DefaultRegexOptions | RegexOptions.Singleline),
|
|
|
|
);
|
|
|
|
(s, m) => new FormattingNode(FormattingKind.Italic, Parse(s.Relocate(m.Groups[1])))
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> UnderlineFormattingNodeMatcher = new RegexMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> UnderlineFormattingNodeMatcher =
|
|
|
|
// There must be exactly two closing underscores.
|
|
|
|
new RegexMatcher<MarkdownNode>(
|
|
|
|
new Regex(@"__(.+?)__(?!_)", DefaultRegexOptions | RegexOptions.Singleline),
|
|
|
|
// There must be exactly two closing underscores.
|
|
|
|
(s, m) => new FormattingNode(FormattingKind.Underline, Parse(s.Relocate(m.Groups[1])))
|
|
|
|
new Regex(@"__(.+?)__(?!_)", DefaultRegexOptions | RegexOptions.Singleline),
|
|
|
|
);
|
|
|
|
(s, m) => new FormattingNode(FormattingKind.Underline, Parse(s.Relocate(m.Groups[1])))
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> ItalicUnderlineFormattingNodeMatcher =
|
|
|
|
private static readonly IMatcher<MarkdownNode> ItalicUnderlineFormattingNodeMatcher =
|
|
|
|
new RegexMatcher<MarkdownNode>(
|
|
|
|
new RegexMatcher<MarkdownNode>(
|
|
|
|
// There must be exactly three closing underscores.
|
|
|
|
// There must be exactly three closing underscores.
|
|
|
|
new Regex(@"_(__.+?__)_(?!_)", DefaultRegexOptions | RegexOptions.Singleline),
|
|
|
|
new Regex(@"_(__.+?__)_(?!_)", DefaultRegexOptions | RegexOptions.Singleline),
|
|
|
|
(s, m) => new FormattingNode(
|
|
|
|
(s, m) =>
|
|
|
|
FormattingKind.Italic,
|
|
|
|
new FormattingNode(
|
|
|
|
Parse(s.Relocate(m.Groups[1]), UnderlineFormattingNodeMatcher)
|
|
|
|
FormattingKind.Italic,
|
|
|
|
)
|
|
|
|
Parse(s.Relocate(m.Groups[1]), UnderlineFormattingNodeMatcher)
|
|
|
|
|
|
|
|
)
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> StrikethroughFormattingNodeMatcher = new RegexMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> StrikethroughFormattingNodeMatcher =
|
|
|
|
new Regex(@"~~(.+?)~~", DefaultRegexOptions | RegexOptions.Singleline),
|
|
|
|
new RegexMatcher<MarkdownNode>(
|
|
|
|
(s, m) => new FormattingNode(FormattingKind.Strikethrough, Parse(s.Relocate(m.Groups[1])))
|
|
|
|
new Regex(@"~~(.+?)~~", DefaultRegexOptions | RegexOptions.Singleline),
|
|
|
|
);
|
|
|
|
(s, m) =>
|
|
|
|
|
|
|
|
new FormattingNode(FormattingKind.Strikethrough, Parse(s.Relocate(m.Groups[1])))
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> SpoilerFormattingNodeMatcher = new RegexMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> SpoilerFormattingNodeMatcher =
|
|
|
|
new Regex(@"\|\|(.+?)\|\|", DefaultRegexOptions | RegexOptions.Singleline),
|
|
|
|
new RegexMatcher<MarkdownNode>(
|
|
|
|
(s, m) => new FormattingNode(FormattingKind.Spoiler, Parse(s.Relocate(m.Groups[1])))
|
|
|
|
new Regex(@"\|\|(.+?)\|\|", DefaultRegexOptions | RegexOptions.Singleline),
|
|
|
|
);
|
|
|
|
(s, m) => new FormattingNode(FormattingKind.Spoiler, Parse(s.Relocate(m.Groups[1])))
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> SingleLineQuoteNodeMatcher = new RegexMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> SingleLineQuoteNodeMatcher =
|
|
|
|
// Include the linebreak in the content so that the lines are preserved in quotes.
|
|
|
|
new RegexMatcher<MarkdownNode>(
|
|
|
|
new Regex(@"^>\s(.+\n?)", DefaultRegexOptions),
|
|
|
|
// Include the linebreak in the content so that the lines are preserved in quotes.
|
|
|
|
(s, m) => new FormattingNode(FormattingKind.Quote, Parse(s.Relocate(m.Groups[1])))
|
|
|
|
new Regex(@"^>\s(.+\n?)", DefaultRegexOptions),
|
|
|
|
);
|
|
|
|
(s, m) => new FormattingNode(FormattingKind.Quote, Parse(s.Relocate(m.Groups[1])))
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> RepeatedSingleLineQuoteNodeMatcher = new RegexMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> RepeatedSingleLineQuoteNodeMatcher =
|
|
|
|
// Include the linebreaks in the content, so that the lines are preserved in quotes.
|
|
|
|
new RegexMatcher<MarkdownNode>(
|
|
|
|
// Empty content is allowed within quotes.
|
|
|
|
// Include the linebreaks in the content, so that the lines are preserved in quotes.
|
|
|
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/1115
|
|
|
|
// Empty content is allowed within quotes.
|
|
|
|
new Regex(@"(?:^>\s(.*\n?)){2,}", DefaultRegexOptions),
|
|
|
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/1115
|
|
|
|
(s, m) => new FormattingNode(
|
|
|
|
new Regex(@"(?:^>\s(.*\n?)){2,}", DefaultRegexOptions),
|
|
|
|
FormattingKind.Quote,
|
|
|
|
(s, m) =>
|
|
|
|
m.Groups[1].Captures.SelectMany(c => Parse(s.Relocate(c))).ToArray()
|
|
|
|
new FormattingNode(
|
|
|
|
)
|
|
|
|
FormattingKind.Quote,
|
|
|
|
);
|
|
|
|
m.Groups[1].Captures.SelectMany(c => Parse(s.Relocate(c))).ToArray()
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> MultiLineQuoteNodeMatcher = new RegexMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> MultiLineQuoteNodeMatcher =
|
|
|
|
new Regex(@"^>>>\s(.+)", DefaultRegexOptions | RegexOptions.Singleline),
|
|
|
|
new RegexMatcher<MarkdownNode>(
|
|
|
|
(s, m) => new FormattingNode(FormattingKind.Quote, Parse(s.Relocate(m.Groups[1])))
|
|
|
|
new Regex(@"^>>>\s(.+)", DefaultRegexOptions | RegexOptions.Singleline),
|
|
|
|
);
|
|
|
|
(s, m) => new FormattingNode(FormattingKind.Quote, Parse(s.Relocate(m.Groups[1])))
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> HeadingNodeMatcher = new RegexMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> HeadingNodeMatcher =
|
|
|
|
// Consume the linebreak so that it's not attached to following nodes.
|
|
|
|
new RegexMatcher<MarkdownNode>(
|
|
|
|
new Regex(@"^(\#{1,3})\s(.+)\n", DefaultRegexOptions),
|
|
|
|
// Consume the linebreak so that it's not attached to following nodes.
|
|
|
|
(s, m) => new HeadingNode(m.Groups[1].Length, Parse(s.Relocate(m.Groups[2])))
|
|
|
|
new Regex(@"^(\#{1,3})\s(.+)\n", DefaultRegexOptions),
|
|
|
|
);
|
|
|
|
(s, m) => new HeadingNode(m.Groups[1].Length, Parse(s.Relocate(m.Groups[2])))
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> ListNodeMatcher = new RegexMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> ListNodeMatcher = new RegexMatcher<MarkdownNode>(
|
|
|
|
// Can be preceded by whitespace, which specifies the list's nesting level.
|
|
|
|
// Can be preceded by whitespace, which specifies the list's nesting level.
|
|
|
|
// Following lines that start with (level+1) whitespace are considered part of the list item.
|
|
|
|
// Following lines that start with (level+1) whitespace are considered part of the list item.
|
|
|
|
// Consume the linebreak so that it's not attached to following nodes.
|
|
|
|
// Consume the linebreak so that it's not attached to following nodes.
|
|
|
|
new Regex(@"^(\s*)(?:[\-\*]\s(.+(?:\n\s\1.*)*)?\n?)+", DefaultRegexOptions),
|
|
|
|
new Regex(@"^(\s*)(?:[\-\*]\s(.+(?:\n\s\1.*)*)?\n?)+", DefaultRegexOptions),
|
|
|
|
(s, m) => new ListNode(
|
|
|
|
(s, m) =>
|
|
|
|
m.Groups[2].Captures.Select(c => new ListItemNode(Parse(s.Relocate(c)))).ToArray()
|
|
|
|
new ListNode(
|
|
|
|
)
|
|
|
|
m.Groups[2].Captures.Select(c => new ListItemNode(Parse(s.Relocate(c)))).ToArray()
|
|
|
|
|
|
|
|
)
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
/* Code blocks */
|
|
|
|
/* Code blocks */
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> InlineCodeBlockNodeMatcher = new RegexMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> InlineCodeBlockNodeMatcher =
|
|
|
|
// One or two backticks are allowed, but they must match on both sides.
|
|
|
|
new RegexMatcher<MarkdownNode>(
|
|
|
|
new Regex(@"(`{1,2})([^`]+)\1", DefaultRegexOptions | RegexOptions.Singleline),
|
|
|
|
// One or two backticks are allowed, but they must match on both sides.
|
|
|
|
(_, m) => new InlineCodeBlockNode(m.Groups[2].Value)
|
|
|
|
new Regex(@"(`{1,2})([^`]+)\1", DefaultRegexOptions | RegexOptions.Singleline),
|
|
|
|
);
|
|
|
|
(_, m) => new InlineCodeBlockNode(m.Groups[2].Value)
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> MultiLineCodeBlockNodeMatcher = new RegexMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> MultiLineCodeBlockNodeMatcher =
|
|
|
|
// Language identifier is one word immediately after opening backticks, followed immediately by a linebreak.
|
|
|
|
new RegexMatcher<MarkdownNode>(
|
|
|
|
// Blank lines at the beginning and at the end of content are trimmed.
|
|
|
|
// Language identifier is one word immediately after opening backticks, followed immediately by a linebreak.
|
|
|
|
new Regex(@"```(?:(\w*)\n)?(.+?)```", DefaultRegexOptions | RegexOptions.Singleline),
|
|
|
|
// Blank lines at the beginning and at the end of content are trimmed.
|
|
|
|
(_, m) => new MultiLineCodeBlockNode(m.Groups[1].Value, m.Groups[2].Value.Trim('\r', '\n'))
|
|
|
|
new Regex(@"```(?:(\w*)\n)?(.+?)```", DefaultRegexOptions | RegexOptions.Singleline),
|
|
|
|
);
|
|
|
|
(_, m) =>
|
|
|
|
|
|
|
|
new MultiLineCodeBlockNode(m.Groups[1].Value, m.Groups[2].Value.Trim('\r', '\n'))
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
/* Mentions */
|
|
|
|
/* Mentions */
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> EveryoneMentionNodeMatcher = new StringMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> EveryoneMentionNodeMatcher =
|
|
|
|
"@everyone",
|
|
|
|
new StringMatcher<MarkdownNode>(
|
|
|
|
_ => new MentionNode(null, MentionKind.Everyone)
|
|
|
|
"@everyone",
|
|
|
|
);
|
|
|
|
_ => new MentionNode(null, MentionKind.Everyone)
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> HereMentionNodeMatcher = new StringMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> HereMentionNodeMatcher =
|
|
|
|
"@here",
|
|
|
|
new StringMatcher<MarkdownNode>("@here", _ => new MentionNode(null, MentionKind.Here));
|
|
|
|
_ => new MentionNode(null, MentionKind.Here)
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> UserMentionNodeMatcher = new RegexMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> UserMentionNodeMatcher =
|
|
|
|
// Capture <@123456> or <@!123456>
|
|
|
|
new RegexMatcher<MarkdownNode>(
|
|
|
|
new Regex(@"<@!?(\d+)>", DefaultRegexOptions),
|
|
|
|
// Capture <@123456> or <@!123456>
|
|
|
|
(_, m) => new MentionNode(Snowflake.TryParse(m.Groups[1].Value), MentionKind.User)
|
|
|
|
new Regex(@"<@!?(\d+)>", DefaultRegexOptions),
|
|
|
|
);
|
|
|
|
(_, m) => new MentionNode(Snowflake.TryParse(m.Groups[1].Value), MentionKind.User)
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> ChannelMentionNodeMatcher = new RegexMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> ChannelMentionNodeMatcher =
|
|
|
|
// Capture <#123456>
|
|
|
|
new RegexMatcher<MarkdownNode>(
|
|
|
|
new Regex(@"<\#!?(\d+)>", DefaultRegexOptions),
|
|
|
|
// Capture <#123456>
|
|
|
|
(_, m) => new MentionNode(Snowflake.TryParse(m.Groups[1].Value), MentionKind.Channel)
|
|
|
|
new Regex(@"<\#!?(\d+)>", DefaultRegexOptions),
|
|
|
|
);
|
|
|
|
(_, m) => new MentionNode(Snowflake.TryParse(m.Groups[1].Value), MentionKind.Channel)
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> RoleMentionNodeMatcher = new RegexMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> RoleMentionNodeMatcher =
|
|
|
|
// Capture <@&123456>
|
|
|
|
new RegexMatcher<MarkdownNode>(
|
|
|
|
new Regex(@"<@&(\d+)>", DefaultRegexOptions),
|
|
|
|
// Capture <@&123456>
|
|
|
|
(_, m) => new MentionNode(Snowflake.TryParse(m.Groups[1].Value), MentionKind.Role)
|
|
|
|
new Regex(@"<@&(\d+)>", DefaultRegexOptions),
|
|
|
|
);
|
|
|
|
(_, m) => new MentionNode(Snowflake.TryParse(m.Groups[1].Value), MentionKind.Role)
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
/* Emoji */
|
|
|
|
/* Emoji */
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> StandardEmojiNodeMatcher = new RegexMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> StandardEmojiNodeMatcher =
|
|
|
|
new Regex(
|
|
|
|
new RegexMatcher<MarkdownNode>(
|
|
|
|
@"(" +
|
|
|
|
new Regex(
|
|
|
|
// Country flag emoji (two regional indicator surrogate pairs)
|
|
|
|
@"("
|
|
|
|
@"(?:\uD83C[\uDDE6-\uDDFF]){2}|" +
|
|
|
|
+
|
|
|
|
// Digit emoji (digit followed by enclosing mark)
|
|
|
|
// Country flag emoji (two regional indicator surrogate pairs)
|
|
|
|
@"\d\p{Me}|" +
|
|
|
|
@"(?:\uD83C[\uDDE6-\uDDFF]){2}|"
|
|
|
|
// Surrogate pair
|
|
|
|
+
|
|
|
|
@"\p{Cs}{2}|" +
|
|
|
|
// Digit emoji (digit followed by enclosing mark)
|
|
|
|
// Miscellaneous characters
|
|
|
|
@"\d\p{Me}|"
|
|
|
|
@"[" +
|
|
|
|
+
|
|
|
|
@"\u2600-\u2604" +
|
|
|
|
// Surrogate pair
|
|
|
|
@"\u260E\u2611" +
|
|
|
|
@"\p{Cs}{2}|"
|
|
|
|
@"\u2614-\u2615" +
|
|
|
|
+
|
|
|
|
@"\u2618\u261D\u2620" +
|
|
|
|
// Miscellaneous characters
|
|
|
|
@"\u2622-\u2623" +
|
|
|
|
@"["
|
|
|
|
@"\u2626\u262A" +
|
|
|
|
+ @"\u2600-\u2604"
|
|
|
|
@"\u262E-\u262F" +
|
|
|
|
+ @"\u260E\u2611"
|
|
|
|
@"\u2638-\u263A" +
|
|
|
|
+ @"\u2614-\u2615"
|
|
|
|
@"\u2640\u2642" +
|
|
|
|
+ @"\u2618\u261D\u2620"
|
|
|
|
@"\u2648-\u2653" +
|
|
|
|
+ @"\u2622-\u2623"
|
|
|
|
@"\u265F-\u2660" +
|
|
|
|
+ @"\u2626\u262A"
|
|
|
|
@"\u2663" +
|
|
|
|
+ @"\u262E-\u262F"
|
|
|
|
@"\u2665-\u2666" +
|
|
|
|
+ @"\u2638-\u263A"
|
|
|
|
@"\u2668\u267B" +
|
|
|
|
+ @"\u2640\u2642"
|
|
|
|
@"\u267E-\u267F" +
|
|
|
|
+ @"\u2648-\u2653"
|
|
|
|
@"\u2692-\u2697" +
|
|
|
|
+ @"\u265F-\u2660"
|
|
|
|
@"\u2699" +
|
|
|
|
+ @"\u2663"
|
|
|
|
@"\u269B-\u269C" +
|
|
|
|
+ @"\u2665-\u2666"
|
|
|
|
@"\u26A0-\u26A1" +
|
|
|
|
+ @"\u2668\u267B"
|
|
|
|
@"\u26A7" +
|
|
|
|
+ @"\u267E-\u267F"
|
|
|
|
@"\u26AA-\u26AB" +
|
|
|
|
+ @"\u2692-\u2697"
|
|
|
|
@"\u26B0-\u26B1" +
|
|
|
|
+ @"\u2699"
|
|
|
|
@"\u26BD-\u26BE" +
|
|
|
|
+ @"\u269B-\u269C"
|
|
|
|
@"\u26C4-\u26C5" +
|
|
|
|
+ @"\u26A0-\u26A1"
|
|
|
|
@"\u26C8" +
|
|
|
|
+ @"\u26A7"
|
|
|
|
@"\u26CE-\u26CF" +
|
|
|
|
+ @"\u26AA-\u26AB"
|
|
|
|
@"\u26D1" +
|
|
|
|
+ @"\u26B0-\u26B1"
|
|
|
|
@"\u26D3-\u26D4" +
|
|
|
|
+ @"\u26BD-\u26BE"
|
|
|
|
@"\u26E9-\u26EA" +
|
|
|
|
+ @"\u26C4-\u26C5"
|
|
|
|
@"\u26F0-\u26F5" +
|
|
|
|
+ @"\u26C8"
|
|
|
|
@"\u26F7-\u26FA" +
|
|
|
|
+ @"\u26CE-\u26CF"
|
|
|
|
@"\u26FD" +
|
|
|
|
+ @"\u26D1"
|
|
|
|
@"]" +
|
|
|
|
+ @"\u26D3-\u26D4"
|
|
|
|
@")", DefaultRegexOptions),
|
|
|
|
+ @"\u26E9-\u26EA"
|
|
|
|
(_, m) => new EmojiNode(m.Groups[1].Value)
|
|
|
|
+ @"\u26F0-\u26F5"
|
|
|
|
);
|
|
|
|
+ @"\u26F7-\u26FA"
|
|
|
|
|
|
|
|
+ @"\u26FD"
|
|
|
|
|
|
|
|
+ @"]"
|
|
|
|
|
|
|
|
+ @")",
|
|
|
|
|
|
|
|
DefaultRegexOptions
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
(_, m) => new EmojiNode(m.Groups[1].Value)
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> CodedStandardEmojiNodeMatcher = new RegexMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> CodedStandardEmojiNodeMatcher =
|
|
|
|
// Capture :thinking:
|
|
|
|
new RegexMatcher<MarkdownNode>(
|
|
|
|
new Regex(@":([\w_]+):", DefaultRegexOptions),
|
|
|
|
// Capture :thinking:
|
|
|
|
(_, m) => EmojiIndex.TryGetName(m.Groups[1].Value)?.Pipe(n => new EmojiNode(n))
|
|
|
|
new Regex(@":([\w_]+):", DefaultRegexOptions),
|
|
|
|
);
|
|
|
|
(_, m) => EmojiIndex.TryGetName(m.Groups[1].Value)?.Pipe(n => new EmojiNode(n))
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> CustomEmojiNodeMatcher = new RegexMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> CustomEmojiNodeMatcher =
|
|
|
|
// Capture <:lul:123456> or <a:lul:123456>
|
|
|
|
new RegexMatcher<MarkdownNode>(
|
|
|
|
new Regex(@"<(a)?:(.+?):(\d+?)>", DefaultRegexOptions),
|
|
|
|
// Capture <:lul:123456> or <a:lul:123456>
|
|
|
|
(_, m) => new EmojiNode(
|
|
|
|
new Regex(@"<(a)?:(.+?):(\d+?)>", DefaultRegexOptions),
|
|
|
|
Snowflake.TryParse(m.Groups[3].Value),
|
|
|
|
(_, m) =>
|
|
|
|
m.Groups[2].Value,
|
|
|
|
new EmojiNode(
|
|
|
|
!string.IsNullOrWhiteSpace(m.Groups[1].Value)
|
|
|
|
Snowflake.TryParse(m.Groups[3].Value),
|
|
|
|
)
|
|
|
|
m.Groups[2].Value,
|
|
|
|
);
|
|
|
|
!string.IsNullOrWhiteSpace(m.Groups[1].Value)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
/* Links */
|
|
|
|
/* Links */
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> AutoLinkNodeMatcher = new RegexMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> AutoLinkNodeMatcher =
|
|
|
|
// Any non-whitespace character after http:// or https://
|
|
|
|
new RegexMatcher<MarkdownNode>(
|
|
|
|
// until the last punctuation character or whitespace.
|
|
|
|
// Any non-whitespace character after http:// or https://
|
|
|
|
new Regex(@"(https?://\S*[^\.,:;""'\s])", DefaultRegexOptions),
|
|
|
|
// until the last punctuation character or whitespace.
|
|
|
|
(_, m) => new LinkNode(m.Groups[1].Value)
|
|
|
|
new Regex(@"(https?://\S*[^\.,:;""'\s])", DefaultRegexOptions),
|
|
|
|
);
|
|
|
|
(_, m) => new LinkNode(m.Groups[1].Value)
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> HiddenLinkNodeMatcher = new RegexMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> HiddenLinkNodeMatcher =
|
|
|
|
// Same as auto link but also surrounded by angular brackets
|
|
|
|
new RegexMatcher<MarkdownNode>(
|
|
|
|
new Regex(@"<(https?://\S*[^\.,:;""'\s])>", DefaultRegexOptions),
|
|
|
|
// Same as auto link but also surrounded by angular brackets
|
|
|
|
(_, m) => new LinkNode(m.Groups[1].Value)
|
|
|
|
new Regex(@"<(https?://\S*[^\.,:;""'\s])>", DefaultRegexOptions),
|
|
|
|
);
|
|
|
|
(_, m) => new LinkNode(m.Groups[1].Value)
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> MaskedLinkNodeMatcher = new RegexMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> MaskedLinkNodeMatcher =
|
|
|
|
// Capture [title](link)
|
|
|
|
new RegexMatcher<MarkdownNode>(
|
|
|
|
new Regex(@"\[(.+?)\]\((.+?)\)", DefaultRegexOptions),
|
|
|
|
// Capture [title](link)
|
|
|
|
(s, m) => new LinkNode(m.Groups[2].Value, Parse(s.Relocate(m.Groups[1])))
|
|
|
|
new Regex(@"\[(.+?)\]\((.+?)\)", DefaultRegexOptions),
|
|
|
|
);
|
|
|
|
(s, m) => new LinkNode(m.Groups[2].Value, Parse(s.Relocate(m.Groups[1])))
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
/* Text */
|
|
|
|
/* Text */
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> ShrugTextNodeMatcher = new StringMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> ShrugTextNodeMatcher =
|
|
|
|
// Capture the shrug kaomoji.
|
|
|
|
new StringMatcher<MarkdownNode>(
|
|
|
|
// This escapes it from matching for formatting.
|
|
|
|
// Capture the shrug kaomoji.
|
|
|
|
@"¯\_(ツ)_/¯",
|
|
|
|
// This escapes it from matching for formatting.
|
|
|
|
s => new TextNode(s.ToString())
|
|
|
|
@"¯\_(ツ)_/¯",
|
|
|
|
);
|
|
|
|
s => new TextNode(s.ToString())
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> IgnoredEmojiTextNodeMatcher = new RegexMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> IgnoredEmojiTextNodeMatcher =
|
|
|
|
// Capture some specific emoji that don't get rendered.
|
|
|
|
new RegexMatcher<MarkdownNode>(
|
|
|
|
// This escapes them from matching for emoji.
|
|
|
|
// Capture some specific emoji that don't get rendered.
|
|
|
|
new Regex(@"([\u26A7\u2640\u2642\u2695\u267E\u00A9\u00AE\u2122])", DefaultRegexOptions),
|
|
|
|
// This escapes them from matching for emoji.
|
|
|
|
(_, m) => new TextNode(m.Groups[1].Value)
|
|
|
|
new Regex(@"([\u26A7\u2640\u2642\u2695\u267E\u00A9\u00AE\u2122])", DefaultRegexOptions),
|
|
|
|
);
|
|
|
|
(_, m) => new TextNode(m.Groups[1].Value)
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> EscapedSymbolTextNodeMatcher = new RegexMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> EscapedSymbolTextNodeMatcher =
|
|
|
|
// Capture any "symbol/other" character or surrogate pair preceded by a backslash.
|
|
|
|
new RegexMatcher<MarkdownNode>(
|
|
|
|
// This escapes them from matching for emoji.
|
|
|
|
// Capture any "symbol/other" character or surrogate pair preceded by a backslash.
|
|
|
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/230
|
|
|
|
// This escapes them from matching for emoji.
|
|
|
|
new Regex(@"\\(\p{So}|\p{Cs}{2})", DefaultRegexOptions),
|
|
|
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/230
|
|
|
|
(_, m) => new TextNode(m.Groups[1].Value)
|
|
|
|
new Regex(@"\\(\p{So}|\p{Cs}{2})", DefaultRegexOptions),
|
|
|
|
);
|
|
|
|
(_, m) => new TextNode(m.Groups[1].Value)
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> EscapedCharacterTextNodeMatcher = new RegexMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> EscapedCharacterTextNodeMatcher =
|
|
|
|
// Capture any non-whitespace, non latin alphanumeric character preceded by a backslash.
|
|
|
|
new RegexMatcher<MarkdownNode>(
|
|
|
|
// This escapes them from matching for formatting or other tokens.
|
|
|
|
// Capture any non-whitespace, non latin alphanumeric character preceded by a backslash.
|
|
|
|
new Regex(@"\\([^a-zA-Z0-9\s])", DefaultRegexOptions),
|
|
|
|
// This escapes them from matching for formatting or other tokens.
|
|
|
|
(_, m) => new TextNode(m.Groups[1].Value)
|
|
|
|
new Regex(@"\\([^a-zA-Z0-9\s])", DefaultRegexOptions),
|
|
|
|
);
|
|
|
|
(_, m) => new TextNode(m.Groups[1].Value)
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
/* Misc */
|
|
|
|
/* Misc */
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly IMatcher<MarkdownNode> TimestampNodeMatcher = new RegexMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> TimestampNodeMatcher =
|
|
|
|
// Capture <t:12345678> or <t:12345678:R>
|
|
|
|
new RegexMatcher<MarkdownNode>(
|
|
|
|
new Regex(@"<t:(-?\d+)(?::(\w))?>", DefaultRegexOptions),
|
|
|
|
// Capture <t:12345678> or <t:12345678:R>
|
|
|
|
(_, m) =>
|
|
|
|
new Regex(@"<t:(-?\d+)(?::(\w))?>", DefaultRegexOptions),
|
|
|
|
{
|
|
|
|
(_, m) =>
|
|
|
|
try
|
|
|
|
|
|
|
|
{
|
|
|
|
{
|
|
|
|
var instant = DateTimeOffset.UnixEpoch + TimeSpan.FromSeconds(
|
|
|
|
try
|
|
|
|
long.Parse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture)
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var format = m.Groups[2].Value switch
|
|
|
|
|
|
|
|
{
|
|
|
|
{
|
|
|
|
"t" => "h:mm tt",
|
|
|
|
var instant =
|
|
|
|
"T" => "h:mm:ss tt",
|
|
|
|
DateTimeOffset.UnixEpoch
|
|
|
|
"d" => "MM/dd/yyyy",
|
|
|
|
+ TimeSpan.FromSeconds(
|
|
|
|
"D" => "MMMM dd, yyyy",
|
|
|
|
long.Parse(
|
|
|
|
"f" => "MMMM dd, yyyy h:mm tt",
|
|
|
|
m.Groups[1].Value,
|
|
|
|
"F" => "dddd, MMMM dd, yyyy h:mm tt",
|
|
|
|
NumberStyles.Integer,
|
|
|
|
// Relative format is ignored because it doesn't make much sense in a static export
|
|
|
|
CultureInfo.InvariantCulture
|
|
|
|
_ => null
|
|
|
|
)
|
|
|
|
};
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return new TimestampNode(instant, format);
|
|
|
|
var format = m.Groups[2].Value switch
|
|
|
|
}
|
|
|
|
{
|
|
|
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/681
|
|
|
|
"t" => "h:mm tt",
|
|
|
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/766
|
|
|
|
"T" => "h:mm:ss tt",
|
|
|
|
catch (Exception ex) when (ex is FormatException or ArgumentOutOfRangeException or OverflowException)
|
|
|
|
"d" => "MM/dd/yyyy",
|
|
|
|
{
|
|
|
|
"D" => "MMMM dd, yyyy",
|
|
|
|
// For invalid timestamps, Discord renders "Invalid Date" instead of ignoring the markdown
|
|
|
|
"f" => "MMMM dd, yyyy h:mm tt",
|
|
|
|
return TimestampNode.Invalid;
|
|
|
|
"F" => "dddd, MMMM dd, yyyy h:mm tt",
|
|
|
|
|
|
|
|
// Relative format is ignored because it doesn't make much sense in a static export
|
|
|
|
|
|
|
|
_ => null
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return new TimestampNode(instant, format);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/681
|
|
|
|
|
|
|
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/766
|
|
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
|
|
when (ex is FormatException or ArgumentOutOfRangeException or OverflowException)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
// For invalid timestamps, Discord renders "Invalid Date" instead of ignoring the markdown
|
|
|
|
|
|
|
|
return TimestampNode.Invalid;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Matchers that have similar patterns are ordered from most specific to least specific
|
|
|
|
// Matchers that have similar patterns are ordered from most specific to least specific
|
|
|
|
private static readonly IMatcher<MarkdownNode> NodeMatcher = new AggregateMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> NodeMatcher = new AggregateMatcher<MarkdownNode>(
|
|
|
@ -320,7 +372,6 @@ internal static partial class MarkdownParser
|
|
|
|
IgnoredEmojiTextNodeMatcher,
|
|
|
|
IgnoredEmojiTextNodeMatcher,
|
|
|
|
EscapedSymbolTextNodeMatcher,
|
|
|
|
EscapedSymbolTextNodeMatcher,
|
|
|
|
EscapedCharacterTextNodeMatcher,
|
|
|
|
EscapedCharacterTextNodeMatcher,
|
|
|
|
|
|
|
|
|
|
|
|
// Formatting
|
|
|
|
// Formatting
|
|
|
|
ItalicBoldFormattingNodeMatcher,
|
|
|
|
ItalicBoldFormattingNodeMatcher,
|
|
|
|
ItalicUnderlineFormattingNodeMatcher,
|
|
|
|
ItalicUnderlineFormattingNodeMatcher,
|
|
|
@ -335,53 +386,46 @@ internal static partial class MarkdownParser
|
|
|
|
SingleLineQuoteNodeMatcher,
|
|
|
|
SingleLineQuoteNodeMatcher,
|
|
|
|
HeadingNodeMatcher,
|
|
|
|
HeadingNodeMatcher,
|
|
|
|
ListNodeMatcher,
|
|
|
|
ListNodeMatcher,
|
|
|
|
|
|
|
|
|
|
|
|
// Code blocks
|
|
|
|
// Code blocks
|
|
|
|
MultiLineCodeBlockNodeMatcher,
|
|
|
|
MultiLineCodeBlockNodeMatcher,
|
|
|
|
InlineCodeBlockNodeMatcher,
|
|
|
|
InlineCodeBlockNodeMatcher,
|
|
|
|
|
|
|
|
|
|
|
|
// Mentions
|
|
|
|
// Mentions
|
|
|
|
EveryoneMentionNodeMatcher,
|
|
|
|
EveryoneMentionNodeMatcher,
|
|
|
|
HereMentionNodeMatcher,
|
|
|
|
HereMentionNodeMatcher,
|
|
|
|
UserMentionNodeMatcher,
|
|
|
|
UserMentionNodeMatcher,
|
|
|
|
ChannelMentionNodeMatcher,
|
|
|
|
ChannelMentionNodeMatcher,
|
|
|
|
RoleMentionNodeMatcher,
|
|
|
|
RoleMentionNodeMatcher,
|
|
|
|
|
|
|
|
|
|
|
|
// Links
|
|
|
|
// Links
|
|
|
|
MaskedLinkNodeMatcher,
|
|
|
|
MaskedLinkNodeMatcher,
|
|
|
|
AutoLinkNodeMatcher,
|
|
|
|
AutoLinkNodeMatcher,
|
|
|
|
HiddenLinkNodeMatcher,
|
|
|
|
HiddenLinkNodeMatcher,
|
|
|
|
|
|
|
|
|
|
|
|
// Emoji
|
|
|
|
// Emoji
|
|
|
|
StandardEmojiNodeMatcher,
|
|
|
|
StandardEmojiNodeMatcher,
|
|
|
|
CustomEmojiNodeMatcher,
|
|
|
|
CustomEmojiNodeMatcher,
|
|
|
|
CodedStandardEmojiNodeMatcher,
|
|
|
|
CodedStandardEmojiNodeMatcher,
|
|
|
|
|
|
|
|
|
|
|
|
// Misc
|
|
|
|
// Misc
|
|
|
|
TimestampNodeMatcher
|
|
|
|
TimestampNodeMatcher
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Minimal set of matchers for non-multimedia formats (e.g. plain text)
|
|
|
|
// Minimal set of matchers for non-multimedia formats (e.g. plain text)
|
|
|
|
private static readonly IMatcher<MarkdownNode> MinimalNodeMatcher = new AggregateMatcher<MarkdownNode>(
|
|
|
|
private static readonly IMatcher<MarkdownNode> MinimalNodeMatcher =
|
|
|
|
// Mentions
|
|
|
|
new AggregateMatcher<MarkdownNode>(
|
|
|
|
EveryoneMentionNodeMatcher,
|
|
|
|
// Mentions
|
|
|
|
HereMentionNodeMatcher,
|
|
|
|
EveryoneMentionNodeMatcher,
|
|
|
|
UserMentionNodeMatcher,
|
|
|
|
HereMentionNodeMatcher,
|
|
|
|
ChannelMentionNodeMatcher,
|
|
|
|
UserMentionNodeMatcher,
|
|
|
|
RoleMentionNodeMatcher,
|
|
|
|
ChannelMentionNodeMatcher,
|
|
|
|
|
|
|
|
RoleMentionNodeMatcher,
|
|
|
|
// Emoji
|
|
|
|
// Emoji
|
|
|
|
CustomEmojiNodeMatcher,
|
|
|
|
CustomEmojiNodeMatcher,
|
|
|
|
|
|
|
|
// Misc
|
|
|
|
// Misc
|
|
|
|
TimestampNodeMatcher
|
|
|
|
TimestampNodeMatcher
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static IReadOnlyList<MarkdownNode> Parse(StringSegment segment, IMatcher<MarkdownNode> matcher) =>
|
|
|
|
private static IReadOnlyList<MarkdownNode> Parse(
|
|
|
|
matcher
|
|
|
|
StringSegment segment,
|
|
|
|
.MatchAll(segment, s => new TextNode(s.ToString()))
|
|
|
|
IMatcher<MarkdownNode> matcher
|
|
|
|
.Select(r => r.Value)
|
|
|
|
) => matcher.MatchAll(segment, s => new TextNode(s.ToString())).Select(r => r.Value).ToArray();
|
|
|
|
.ToArray();
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
internal static partial class MarkdownParser
|
|
|
|
internal static partial class MarkdownParser
|
|
|
|