mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2025-05-11 10:26:57 +02:00
Include inline emoji in JSON export (#1311)
This commit is contained in:
parent
9c15baf799
commit
789e5af8ba
7 changed files with 204 additions and 87 deletions
|
@ -10,6 +10,8 @@ public static class ChannelIds
|
||||||
|
|
||||||
public static Snowflake EmbedTestCases { get; } = Snowflake.Parse("866472452459462687");
|
public static Snowflake EmbedTestCases { get; } = Snowflake.Parse("866472452459462687");
|
||||||
|
|
||||||
|
public static Snowflake EmojiTestCases { get; } = Snowflake.Parse("866768438290415636");
|
||||||
|
|
||||||
public static Snowflake GroupingTestCases { get; } = Snowflake.Parse("992092091545034842");
|
public static Snowflake GroupingTestCases { get; } = Snowflake.Parse("992092091545034842");
|
||||||
|
|
||||||
public static Snowflake FilterTestCases { get; } = Snowflake.Parse("866744075033641020");
|
public static Snowflake FilterTestCases { get; } = Snowflake.Parse("866744075033641020");
|
||||||
|
|
69
DiscordChatExporter.Cli.Tests/Specs/JsonEmojiSpecs.cs
Normal file
69
DiscordChatExporter.Cli.Tests/Specs/JsonEmojiSpecs.cs
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
|
using DiscordChatExporter.Core.Discord;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Cli.Tests.Specs;
|
||||||
|
|
||||||
|
public class JsonEmojiSpecs
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task I_can_export_a_channel_that_contains_a_message_with_inline_emoji_and_have_them_listed_separately()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var message = await ExportWrapper.GetMessageAsJsonAsync(
|
||||||
|
ChannelIds.EmojiTestCases,
|
||||||
|
Snowflake.Parse("866768521052553216")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var inlineEmojis = message.GetProperty("inlineEmojis").EnumerateArray().ToArray();
|
||||||
|
inlineEmojis.Should().HaveCount(4);
|
||||||
|
|
||||||
|
inlineEmojis[0].GetProperty("id").GetString().Should().BeNullOrEmpty();
|
||||||
|
inlineEmojis[0].GetProperty("name").GetString().Should().Be("🙂");
|
||||||
|
inlineEmojis[0].GetProperty("code").GetString().Should().Be("slight_smile");
|
||||||
|
inlineEmojis[0].GetProperty("isAnimated").GetBoolean().Should().BeFalse();
|
||||||
|
inlineEmojis[0].GetProperty("imageUrl").GetString().Should().NotBeNullOrWhiteSpace();
|
||||||
|
|
||||||
|
inlineEmojis[1].GetProperty("id").GetString().Should().BeNullOrEmpty();
|
||||||
|
inlineEmojis[1].GetProperty("name").GetString().Should().Be("😦");
|
||||||
|
inlineEmojis[1].GetProperty("code").GetString().Should().Be("frowning");
|
||||||
|
inlineEmojis[1].GetProperty("isAnimated").GetBoolean().Should().BeFalse();
|
||||||
|
inlineEmojis[1].GetProperty("imageUrl").GetString().Should().NotBeNullOrWhiteSpace();
|
||||||
|
|
||||||
|
inlineEmojis[2].GetProperty("id").GetString().Should().BeNullOrEmpty();
|
||||||
|
inlineEmojis[2].GetProperty("name").GetString().Should().Be("😔");
|
||||||
|
inlineEmojis[2].GetProperty("code").GetString().Should().Be("pensive");
|
||||||
|
inlineEmojis[2].GetProperty("isAnimated").GetBoolean().Should().BeFalse();
|
||||||
|
inlineEmojis[2].GetProperty("imageUrl").GetString().Should().NotBeNullOrWhiteSpace();
|
||||||
|
|
||||||
|
inlineEmojis[3].GetProperty("id").GetString().Should().BeNullOrEmpty();
|
||||||
|
inlineEmojis[3].GetProperty("name").GetString().Should().Be("😂");
|
||||||
|
inlineEmojis[3].GetProperty("code").GetString().Should().Be("joy");
|
||||||
|
inlineEmojis[3].GetProperty("isAnimated").GetBoolean().Should().BeFalse();
|
||||||
|
inlineEmojis[3].GetProperty("imageUrl").GetString().Should().NotBeNullOrWhiteSpace();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task I_can_export_a_channel_that_contains_a_message_with_custom_inline_emoji_and_have_them_listed_separately()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var message = await ExportWrapper.GetMessageAsJsonAsync(
|
||||||
|
ChannelIds.EmojiTestCases,
|
||||||
|
Snowflake.Parse("1299804867447230594")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var inlineEmojis = message.GetProperty("inlineEmojis").EnumerateArray().ToArray();
|
||||||
|
inlineEmojis.Should().HaveCount(1);
|
||||||
|
|
||||||
|
inlineEmojis[0].GetProperty("id").GetString().Should().Be("754441880066064584");
|
||||||
|
inlineEmojis[0].GetProperty("name").GetString().Should().Be("lemon_blush");
|
||||||
|
inlineEmojis[0].GetProperty("code").GetString().Should().Be("lemon_blush");
|
||||||
|
inlineEmojis[0].GetProperty("isAnimated").GetBoolean().Should().BeFalse();
|
||||||
|
inlineEmojis[0].GetProperty("imageUrl").GetString().Should().NotBeNullOrWhiteSpace();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
using System;
|
using System.Text.Json;
|
||||||
using System.Text.Json;
|
|
||||||
using DiscordChatExporter.Core.Discord.Data.Common;
|
using DiscordChatExporter.Core.Discord.Data.Common;
|
||||||
using DiscordChatExporter.Core.Utils;
|
using DiscordChatExporter.Core.Utils;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
|
@ -13,29 +12,22 @@ public partial record Emoji(
|
||||||
Snowflake? Id,
|
Snowflake? Id,
|
||||||
// Name of a custom emoji (e.g. LUL) or actual representation of a standard emoji (e.g. 🙂)
|
// Name of a custom emoji (e.g. LUL) or actual representation of a standard emoji (e.g. 🙂)
|
||||||
string Name,
|
string Name,
|
||||||
bool IsAnimated,
|
bool IsAnimated
|
||||||
string ImageUrl
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
public bool IsCustomEmoji { get; } = Id is not null;
|
||||||
|
|
||||||
// Name of a custom emoji (e.g. LUL) or name of a standard emoji (e.g. slight_smile)
|
// Name of a custom emoji (e.g. LUL) or name of a standard emoji (e.g. slight_smile)
|
||||||
public string Code => Id is not null ? Name : EmojiIndex.TryGetCode(Name) ?? Name;
|
public string Code { get; } = Id is not null ? Name : EmojiIndex.TryGetCode(Name) ?? Name;
|
||||||
|
|
||||||
|
public string ImageUrl { get; } =
|
||||||
|
Id is not null
|
||||||
|
? ImageCdn.GetCustomEmojiUrl(Id.Value, IsAnimated)
|
||||||
|
: ImageCdn.GetStandardEmojiUrl(Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial record Emoji
|
public partial record Emoji
|
||||||
{
|
{
|
||||||
public static string GetImageUrl(Snowflake? id, string? name, bool isAnimated)
|
|
||||||
{
|
|
||||||
// Custom emoji
|
|
||||||
if (id is not null)
|
|
||||||
return ImageCdn.GetCustomEmojiUrl(id.Value, isAnimated);
|
|
||||||
|
|
||||||
// Standard emoji
|
|
||||||
if (!string.IsNullOrWhiteSpace(name))
|
|
||||||
return ImageCdn.GetStandardEmojiUrl(name);
|
|
||||||
|
|
||||||
throw new InvalidOperationException("Either the emoji ID or name should be provided.");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Emoji Parse(JsonElement json)
|
public static Emoji Parse(JsonElement json)
|
||||||
{
|
{
|
||||||
var id = json.GetPropertyOrNull("id")
|
var id = json.GetPropertyOrNull("id")
|
||||||
|
@ -47,8 +39,7 @@ public partial record Emoji
|
||||||
json.GetPropertyOrNull("name")?.GetNonWhiteSpaceStringOrNull() ?? "Unknown Emoji";
|
json.GetPropertyOrNull("name")?.GetNonWhiteSpaceStringOrNull() ?? "Unknown Emoji";
|
||||||
|
|
||||||
var isAnimated = json.GetPropertyOrNull("animated")?.GetBooleanOrNull() ?? false;
|
var isAnimated = json.GetPropertyOrNull("animated")?.GetBooleanOrNull() ?? false;
|
||||||
var imageUrl = GetImageUrl(id, name, isAnimated);
|
|
||||||
|
|
||||||
return new Emoji(id, name, isAnimated, imageUrl);
|
return new Emoji(id, name, isAnimated);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Core.Discord.Data;
|
|
||||||
using DiscordChatExporter.Core.Markdown;
|
using DiscordChatExporter.Core.Markdown;
|
||||||
using DiscordChatExporter.Core.Markdown.Parsing;
|
using DiscordChatExporter.Core.Markdown.Parsing;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
|
@ -210,7 +209,6 @@ internal partial class HtmlMarkdownVisitor(
|
||||||
CancellationToken cancellationToken = default
|
CancellationToken cancellationToken = default
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var emojiImageUrl = Emoji.GetImageUrl(emoji.Id, emoji.Name, emoji.IsAnimated);
|
|
||||||
var jumboClass = isJumbo ? "chatlog__emoji--large" : "";
|
var jumboClass = isJumbo ? "chatlog__emoji--large" : "";
|
||||||
|
|
||||||
buffer.Append(
|
buffer.Append(
|
||||||
|
@ -221,7 +219,7 @@ internal partial class HtmlMarkdownVisitor(
|
||||||
class="chatlog__emoji {jumboClass}"
|
class="chatlog__emoji {jumboClass}"
|
||||||
alt="{emoji.Name}"
|
alt="{emoji.Name}"
|
||||||
title="{emoji.Code}"
|
title="{emoji.Code}"
|
||||||
src="{await context.ResolveAssetUrlAsync(emojiImageUrl, cancellationToken)}">
|
src="{await context.ResolveAssetUrlAsync(emoji.ImageUrl, cancellationToken)}">
|
||||||
"""
|
"""
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Core.Discord.Data;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
using DiscordChatExporter.Core.Discord.Data.Embeds;
|
using DiscordChatExporter.Core.Discord.Data.Embeds;
|
||||||
|
using DiscordChatExporter.Core.Markdown.Parsing;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
using JsonExtensions.Writing;
|
using JsonExtensions.Writing;
|
||||||
|
|
||||||
|
@ -37,22 +39,31 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
|
||||||
? await PlainTextMarkdownVisitor.FormatAsync(Context, markdown, cancellationToken)
|
? await PlainTextMarkdownVisitor.FormatAsync(Context, markdown, cancellationToken)
|
||||||
: markdown;
|
: markdown;
|
||||||
|
|
||||||
private async ValueTask WriteUserAsync(User user, CancellationToken cancellationToken = default)
|
private async ValueTask WriteUserAsync(
|
||||||
|
User user,
|
||||||
|
bool includeRoles = true,
|
||||||
|
CancellationToken cancellationToken = default
|
||||||
|
)
|
||||||
{
|
{
|
||||||
_writer.WriteStartObject();
|
_writer.WriteStartObject();
|
||||||
|
|
||||||
_writer.WriteString("id", user.Id.ToString());
|
_writer.WriteString("id", user.Id.ToString());
|
||||||
_writer.WriteString("name", user.Name);
|
_writer.WriteString("name", user.Name);
|
||||||
_writer.WriteString("discriminator", user.DiscriminatorFormatted);
|
_writer.WriteString("discriminator", user.DiscriminatorFormatted);
|
||||||
|
|
||||||
_writer.WriteString(
|
_writer.WriteString(
|
||||||
"nickname",
|
"nickname",
|
||||||
Context.TryGetMember(user.Id)?.DisplayName ?? user.DisplayName
|
Context.TryGetMember(user.Id)?.DisplayName ?? user.DisplayName
|
||||||
);
|
);
|
||||||
|
|
||||||
_writer.WriteString("color", Context.TryGetUserColor(user.Id)?.ToHex());
|
_writer.WriteString("color", Context.TryGetUserColor(user.Id)?.ToHex());
|
||||||
_writer.WriteBoolean("isBot", user.IsBot);
|
_writer.WriteBoolean("isBot", user.IsBot);
|
||||||
|
|
||||||
_writer.WritePropertyName("roles");
|
if (includeRoles)
|
||||||
await WriteRolesAsync(Context.GetUserRoles(user.Id), cancellationToken);
|
{
|
||||||
|
_writer.WritePropertyName("roles");
|
||||||
|
await WriteRolesAsync(Context.GetUserRoles(user.Id), cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
_writer.WriteString(
|
_writer.WriteString(
|
||||||
"avatarUrl",
|
"avatarUrl",
|
||||||
|
@ -66,6 +77,26 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
|
||||||
await _writer.FlushAsync(cancellationToken);
|
await _writer.FlushAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async ValueTask WriteEmojiAsync(
|
||||||
|
Emoji emoji,
|
||||||
|
CancellationToken cancellationToken = default
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_writer.WriteStartObject();
|
||||||
|
|
||||||
|
_writer.WriteString("id", emoji.Id.ToString());
|
||||||
|
_writer.WriteString("name", emoji.Name);
|
||||||
|
_writer.WriteString("code", emoji.Code);
|
||||||
|
_writer.WriteBoolean("isAnimated", emoji.IsAnimated);
|
||||||
|
_writer.WriteString(
|
||||||
|
"imageUrl",
|
||||||
|
await Context.ResolveAssetUrlAsync(emoji.ImageUrl, cancellationToken)
|
||||||
|
);
|
||||||
|
|
||||||
|
_writer.WriteEndObject();
|
||||||
|
await _writer.FlushAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
private async ValueTask WriteRolesAsync(
|
private async ValueTask WriteRolesAsync(
|
||||||
IReadOnlyList<Role> roles,
|
IReadOnlyList<Role> roles,
|
||||||
CancellationToken cancellationToken = default
|
CancellationToken cancellationToken = default
|
||||||
|
@ -273,6 +304,26 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
|
||||||
|
|
||||||
_writer.WriteEndArray();
|
_writer.WriteEndArray();
|
||||||
|
|
||||||
|
// Inline emoji
|
||||||
|
_writer.WriteStartArray("inlineEmojis");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(embed.Description))
|
||||||
|
{
|
||||||
|
foreach (
|
||||||
|
var emoji in MarkdownParser
|
||||||
|
.ExtractEmojis(embed.Description)
|
||||||
|
.DistinctBy(e => e.Name, StringComparer.Ordinal)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
await WriteEmojiAsync(
|
||||||
|
new Emoji(emoji.Id, emoji.Name, emoji.IsAnimated),
|
||||||
|
cancellationToken
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_writer.WriteEndArray();
|
||||||
|
|
||||||
_writer.WriteEndObject();
|
_writer.WriteEndObject();
|
||||||
await _writer.FlushAsync(cancellationToken);
|
await _writer.FlushAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
@ -373,7 +424,7 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
|
||||||
|
|
||||||
// Author
|
// Author
|
||||||
_writer.WritePropertyName("author");
|
_writer.WritePropertyName("author");
|
||||||
await WriteUserAsync(message.Author, cancellationToken);
|
await WriteUserAsync(message.Author, true, cancellationToken);
|
||||||
|
|
||||||
// Attachments
|
// Attachments
|
||||||
_writer.WriteStartArray("attachments");
|
_writer.WriteStartArray("attachments");
|
||||||
|
@ -431,20 +482,14 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
|
||||||
_writer.WriteStartObject();
|
_writer.WriteStartObject();
|
||||||
|
|
||||||
// Emoji
|
// Emoji
|
||||||
_writer.WriteStartObject("emoji");
|
_writer.WritePropertyName("emoji");
|
||||||
_writer.WriteString("id", reaction.Emoji.Id.ToString());
|
await WriteEmojiAsync(reaction.Emoji, cancellationToken);
|
||||||
_writer.WriteString("name", reaction.Emoji.Name);
|
|
||||||
_writer.WriteString("code", reaction.Emoji.Code);
|
|
||||||
_writer.WriteBoolean("isAnimated", reaction.Emoji.IsAnimated);
|
|
||||||
_writer.WriteString(
|
|
||||||
"imageUrl",
|
|
||||||
await Context.ResolveAssetUrlAsync(reaction.Emoji.ImageUrl, cancellationToken)
|
|
||||||
);
|
|
||||||
_writer.WriteEndObject();
|
|
||||||
|
|
||||||
_writer.WriteNumber("count", reaction.Count);
|
_writer.WriteNumber("count", reaction.Count);
|
||||||
|
|
||||||
|
// Reaction authors
|
||||||
_writer.WriteStartArray("users");
|
_writer.WriteStartArray("users");
|
||||||
|
|
||||||
await foreach (
|
await foreach (
|
||||||
var user in Context.Discord.GetMessageReactionsAsync(
|
var user in Context.Discord.GetMessageReactionsAsync(
|
||||||
Context.Request.Channel.Id,
|
Context.Request.Channel.Id,
|
||||||
|
@ -454,28 +499,7 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_writer.WriteStartObject();
|
await WriteUserAsync(user, false, cancellationToken);
|
||||||
|
|
||||||
// Write limited user information without color and roles,
|
|
||||||
// so we can avoid fetching guild member information for each user.
|
|
||||||
_writer.WriteString("id", user.Id.ToString());
|
|
||||||
_writer.WriteString("name", user.Name);
|
|
||||||
_writer.WriteString("discriminator", user.DiscriminatorFormatted);
|
|
||||||
_writer.WriteString(
|
|
||||||
"nickname",
|
|
||||||
Context.TryGetMember(user.Id)?.DisplayName ?? user.DisplayName
|
|
||||||
);
|
|
||||||
_writer.WriteBoolean("isBot", user.IsBot);
|
|
||||||
|
|
||||||
_writer.WriteString(
|
|
||||||
"avatarUrl",
|
|
||||||
await Context.ResolveAssetUrlAsync(
|
|
||||||
Context.TryGetMember(user.Id)?.AvatarUrl ?? user.AvatarUrl,
|
|
||||||
cancellationToken
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
_writer.WriteEndObject();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_writer.WriteEndArray();
|
_writer.WriteEndArray();
|
||||||
|
@ -487,9 +511,8 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
|
||||||
|
|
||||||
// Mentions
|
// Mentions
|
||||||
_writer.WriteStartArray("mentions");
|
_writer.WriteStartArray("mentions");
|
||||||
|
|
||||||
foreach (var user in message.MentionedUsers)
|
foreach (var user in message.MentionedUsers)
|
||||||
await WriteUserAsync(user, cancellationToken);
|
await WriteUserAsync(user, true, cancellationToken);
|
||||||
|
|
||||||
_writer.WriteEndArray();
|
_writer.WriteEndArray();
|
||||||
|
|
||||||
|
@ -512,11 +535,28 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
|
||||||
_writer.WriteString("name", message.Interaction.Name);
|
_writer.WriteString("name", message.Interaction.Name);
|
||||||
|
|
||||||
_writer.WritePropertyName("user");
|
_writer.WritePropertyName("user");
|
||||||
await WriteUserAsync(message.Interaction.User, cancellationToken);
|
await WriteUserAsync(message.Interaction.User, true, cancellationToken);
|
||||||
|
|
||||||
_writer.WriteEndObject();
|
_writer.WriteEndObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inline emoji
|
||||||
|
_writer.WriteStartArray("inlineEmojis");
|
||||||
|
|
||||||
|
foreach (
|
||||||
|
var emoji in MarkdownParser
|
||||||
|
.ExtractEmojis(message.Content)
|
||||||
|
.DistinctBy(e => e.Name, StringComparer.Ordinal)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
await WriteEmojiAsync(
|
||||||
|
new Emoji(emoji.Id, emoji.Name, emoji.IsAnimated),
|
||||||
|
cancellationToken
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_writer.WriteEndArray();
|
||||||
|
|
||||||
_writer.WriteEndObject();
|
_writer.WriteEndObject();
|
||||||
await _writer.FlushAsync(cancellationToken);
|
await _writer.FlushAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using DiscordChatExporter.Core.Utils;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Markdown;
|
namespace DiscordChatExporter.Core.Markdown;
|
||||||
|
|
||||||
|
@ -11,11 +11,17 @@ internal record EmojiNode(
|
||||||
bool IsAnimated
|
bool IsAnimated
|
||||||
) : MarkdownNode
|
) : MarkdownNode
|
||||||
{
|
{
|
||||||
public bool IsCustomEmoji => Id is not null;
|
// This coupling is unsound from the domain-design perspective, but it helps us reuse
|
||||||
|
// some code for now. We can refactor this later, if the coupling becomes a problem.
|
||||||
// Name of a custom emoji (e.g. LUL) or name of a standard emoji (e.g. slight_smile)
|
private readonly Emoji _emoji = new(Id, Name, IsAnimated);
|
||||||
public string Code => IsCustomEmoji ? Name : EmojiIndex.TryGetCode(Name) ?? Name;
|
|
||||||
|
|
||||||
public EmojiNode(string name)
|
public EmojiNode(string name)
|
||||||
: this(null, name, false) { }
|
: this(null, name, false) { }
|
||||||
|
|
||||||
|
public bool IsCustomEmoji => _emoji.IsCustomEmoji;
|
||||||
|
|
||||||
|
// Name of a custom emoji (e.g. LUL) or name of a standard emoji (e.g. slight_smile)
|
||||||
|
public string Code => _emoji.Code;
|
||||||
|
|
||||||
|
public string ImageUrl => _emoji.ImageUrl;
|
||||||
}
|
}
|
||||||
|
|
|
@ -484,6 +484,37 @@ internal static partial class MarkdownParser
|
||||||
|
|
||||||
internal static partial class MarkdownParser
|
internal static partial class MarkdownParser
|
||||||
{
|
{
|
||||||
|
private static void Extract<TNode>(
|
||||||
|
IEnumerable<MarkdownNode> nodes,
|
||||||
|
ICollection<TNode> extractedNodes
|
||||||
|
)
|
||||||
|
where TNode : MarkdownNode
|
||||||
|
{
|
||||||
|
foreach (var node in nodes)
|
||||||
|
{
|
||||||
|
if (node is TNode extractedNode)
|
||||||
|
extractedNodes.Add(extractedNode);
|
||||||
|
|
||||||
|
if (node is IContainerNode containerNode)
|
||||||
|
Extract(containerNode.Children, extractedNodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<TNode> Extract<TNode>(string markdown)
|
||||||
|
where TNode : MarkdownNode
|
||||||
|
{
|
||||||
|
var extractedNodes = new List<TNode>();
|
||||||
|
Extract(Parse(markdown), extractedNodes);
|
||||||
|
|
||||||
|
return extractedNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<LinkNode> ExtractLinks(string markdown) =>
|
||||||
|
Extract<LinkNode>(markdown);
|
||||||
|
|
||||||
|
public static IReadOnlyList<EmojiNode> ExtractEmojis(string markdown) =>
|
||||||
|
Extract<EmojiNode>(markdown);
|
||||||
|
|
||||||
private static IReadOnlyList<MarkdownNode> Parse(
|
private static IReadOnlyList<MarkdownNode> Parse(
|
||||||
MarkdownContext context,
|
MarkdownContext context,
|
||||||
StringSegment segment
|
StringSegment segment
|
||||||
|
@ -499,24 +530,4 @@ internal static partial class MarkdownParser
|
||||||
|
|
||||||
public static IReadOnlyList<MarkdownNode> ParseMinimal(string markdown) =>
|
public static IReadOnlyList<MarkdownNode> ParseMinimal(string markdown) =>
|
||||||
ParseMinimal(new MarkdownContext(), new StringSegment(markdown));
|
ParseMinimal(new MarkdownContext(), new StringSegment(markdown));
|
||||||
|
|
||||||
private static void ExtractLinks(IEnumerable<MarkdownNode> nodes, ICollection<LinkNode> links)
|
|
||||||
{
|
|
||||||
foreach (var node in nodes)
|
|
||||||
{
|
|
||||||
if (node is LinkNode linkNode)
|
|
||||||
links.Add(linkNode);
|
|
||||||
|
|
||||||
if (node is IContainerNode containerNode)
|
|
||||||
ExtractLinks(containerNode.Children, links);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IReadOnlyList<LinkNode> ExtractLinks(string markdown)
|
|
||||||
{
|
|
||||||
var links = new List<LinkNode>();
|
|
||||||
ExtractLinks(Parse(markdown), links);
|
|
||||||
|
|
||||||
return links;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue