|
|
@ -40,33 +40,45 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
|
|
|
var (openingTag, closingTag) = formatting.Kind switch
|
|
|
|
var (openingTag, closingTag) = formatting.Kind switch
|
|
|
|
{
|
|
|
|
{
|
|
|
|
FormattingKind.Bold => (
|
|
|
|
FormattingKind.Bold => (
|
|
|
|
|
|
|
|
// language=HTML
|
|
|
|
"<strong>",
|
|
|
|
"<strong>",
|
|
|
|
|
|
|
|
// language=HTML
|
|
|
|
"</strong>"
|
|
|
|
"</strong>"
|
|
|
|
),
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
FormattingKind.Italic => (
|
|
|
|
FormattingKind.Italic => (
|
|
|
|
|
|
|
|
// language=HTML
|
|
|
|
"<em>",
|
|
|
|
"<em>",
|
|
|
|
|
|
|
|
// language=HTML
|
|
|
|
"</em>"
|
|
|
|
"</em>"
|
|
|
|
),
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
FormattingKind.Underline => (
|
|
|
|
FormattingKind.Underline => (
|
|
|
|
|
|
|
|
// language=HTML
|
|
|
|
"<u>",
|
|
|
|
"<u>",
|
|
|
|
|
|
|
|
// language=HTML
|
|
|
|
"</u>"
|
|
|
|
"</u>"
|
|
|
|
),
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
FormattingKind.Strikethrough => (
|
|
|
|
FormattingKind.Strikethrough => (
|
|
|
|
|
|
|
|
// language=HTML
|
|
|
|
"<s>",
|
|
|
|
"<s>",
|
|
|
|
|
|
|
|
// language=HTML
|
|
|
|
"</s>"
|
|
|
|
"</s>"
|
|
|
|
),
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
FormattingKind.Spoiler => (
|
|
|
|
FormattingKind.Spoiler => (
|
|
|
|
"<span class=\"chatlog__markdown-spoiler chatlog__markdown-spoiler--hidden\" onclick=\"showSpoiler(event, this)\">",
|
|
|
|
// language=HTML
|
|
|
|
"</span>"
|
|
|
|
"""<span class="chatlog__markdown-spoiler chatlog__markdown-spoiler--hidden" onclick="showSpoiler(event, this)">""",
|
|
|
|
|
|
|
|
// language=HTML
|
|
|
|
|
|
|
|
"""</span>"""
|
|
|
|
),
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
FormattingKind.Quote => (
|
|
|
|
FormattingKind.Quote => (
|
|
|
|
"<div class=\"chatlog__markdown-quote\"><div class=\"chatlog__markdown-quote-border\"></div><div class=\"chatlog__markdown-quote-content\">",
|
|
|
|
// language=HTML
|
|
|
|
"</div></div>"
|
|
|
|
"""<div class="chatlog__markdown-quote"><div class="chatlog__markdown-quote-border"></div><div class="chatlog__markdown-quote-content">""",
|
|
|
|
|
|
|
|
// language=HTML
|
|
|
|
|
|
|
|
"""</div></div>"""
|
|
|
|
),
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
_ => throw new InvalidOperationException($"Unknown formatting kind '{formatting.Kind}'.")
|
|
|
|
_ => throw new InvalidOperationException($"Unknown formatting kind '{formatting.Kind}'.")
|
|
|
@ -83,10 +95,12 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
|
|
|
InlineCodeBlockNode inlineCodeBlock,
|
|
|
|
InlineCodeBlockNode inlineCodeBlock,
|
|
|
|
CancellationToken cancellationToken = default)
|
|
|
|
CancellationToken cancellationToken = default)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
_buffer
|
|
|
|
_buffer.Append(
|
|
|
|
.Append("<code class=\"chatlog__markdown-pre chatlog__markdown-pre--inline\">")
|
|
|
|
// language=HTML
|
|
|
|
.Append(HtmlEncode(inlineCodeBlock.Code))
|
|
|
|
$"""
|
|
|
|
.Append("</code>");
|
|
|
|
<code class="chatlog__markdown-pre chatlog__markdown-pre--inline">{HtmlEncode(inlineCodeBlock.Code)}</code>
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return await base.VisitInlineCodeBlockAsync(inlineCodeBlock, cancellationToken);
|
|
|
|
return await base.VisitInlineCodeBlockAsync(inlineCodeBlock, cancellationToken);
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -95,14 +109,16 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
|
|
|
MultiLineCodeBlockNode multiLineCodeBlock,
|
|
|
|
MultiLineCodeBlockNode multiLineCodeBlock,
|
|
|
|
CancellationToken cancellationToken = default)
|
|
|
|
CancellationToken cancellationToken = default)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
var highlightCssClass = !string.IsNullOrWhiteSpace(multiLineCodeBlock.Language)
|
|
|
|
var highlightClass = !string.IsNullOrWhiteSpace(multiLineCodeBlock.Language)
|
|
|
|
? $"language-{multiLineCodeBlock.Language}"
|
|
|
|
? $"language-{multiLineCodeBlock.Language}"
|
|
|
|
: "nohighlight";
|
|
|
|
: "nohighlight";
|
|
|
|
|
|
|
|
|
|
|
|
_buffer
|
|
|
|
_buffer.Append(
|
|
|
|
.Append($"<code class=\"chatlog__markdown-pre chatlog__markdown-pre--multiline {highlightCssClass}\">")
|
|
|
|
// language=HTML
|
|
|
|
.Append(HtmlEncode(multiLineCodeBlock.Code))
|
|
|
|
$"""
|
|
|
|
.Append("</code>");
|
|
|
|
<code class="chatlog__markdown-pre chatlog__markdown-pre--multiline {highlightClass}">{HtmlEncode(multiLineCodeBlock.Code)}</code>
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return await base.VisitMultiLineCodeBlockAsync(multiLineCodeBlock, cancellationToken);
|
|
|
|
return await base.VisitMultiLineCodeBlockAsync(multiLineCodeBlock, cancellationToken);
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -111,7 +127,7 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
|
|
|
LinkNode link,
|
|
|
|
LinkNode link,
|
|
|
|
CancellationToken cancellationToken = default)
|
|
|
|
CancellationToken cancellationToken = default)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
// Try to extract message ID if the link refers to a Discord message
|
|
|
|
// Try to extract the message ID if the link points to a Discord message
|
|
|
|
var linkedMessageId = Regex.Match(
|
|
|
|
var linkedMessageId = Regex.Match(
|
|
|
|
link.Url,
|
|
|
|
link.Url,
|
|
|
|
"^https?://(?:discord|discordapp).com/channels/.*?/(\\d+)/?$"
|
|
|
|
"^https?://(?:discord|discordapp).com/channels/.*?/(\\d+)/?$"
|
|
|
@ -119,11 +135,15 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
|
|
|
|
|
|
|
|
|
|
|
_buffer.Append(
|
|
|
|
_buffer.Append(
|
|
|
|
!string.IsNullOrWhiteSpace(linkedMessageId)
|
|
|
|
!string.IsNullOrWhiteSpace(linkedMessageId)
|
|
|
|
? $"<a href=\"{HtmlEncode(link.Url)}\" onclick=\"scrollToMessage(event, '{linkedMessageId}')\">"
|
|
|
|
// language=HTML
|
|
|
|
: $"<a href=\"{HtmlEncode(link.Url)}\">"
|
|
|
|
? $"""<a href="{HtmlEncode(link.Url)}" onclick="scrollToMessage(event, '{linkedMessageId}')">"""
|
|
|
|
|
|
|
|
// language=HTML
|
|
|
|
|
|
|
|
: $"""<a href="{HtmlEncode(link.Url)}">"""
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
var result = await base.VisitLinkAsync(link, cancellationToken);
|
|
|
|
var result = await base.VisitLinkAsync(link, cancellationToken);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// language=HTML
|
|
|
|
_buffer.Append("</a>");
|
|
|
|
_buffer.Append("</a>");
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
return result;
|
|
|
@ -137,13 +157,15 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
|
|
|
var jumboClass = _isJumbo ? "chatlog__emoji--large" : "";
|
|
|
|
var jumboClass = _isJumbo ? "chatlog__emoji--large" : "";
|
|
|
|
|
|
|
|
|
|
|
|
_buffer.Append(
|
|
|
|
_buffer.Append(
|
|
|
|
$"<img " +
|
|
|
|
// language=HTML
|
|
|
|
$"loading=\"lazy\" " +
|
|
|
|
$"""
|
|
|
|
$"class=\"chatlog__emoji {jumboClass}\" " +
|
|
|
|
<img
|
|
|
|
$"alt=\"{emoji.Name}\" " +
|
|
|
|
loading="lazy"
|
|
|
|
$"title=\"{emoji.Code}\" " +
|
|
|
|
class="chatlog__emoji {jumboClass}"
|
|
|
|
$"src=\"{await _context.ResolveAssetUrlAsync(emojiImageUrl, cancellationToken)}\"" +
|
|
|
|
alt="{emoji.Name}"
|
|
|
|
$">"
|
|
|
|
title="{emoji.Code}"
|
|
|
|
|
|
|
|
src="{await _context.ResolveAssetUrlAsync(emojiImageUrl, cancellationToken)}">
|
|
|
|
|
|
|
|
"""
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return await base.VisitEmojiAsync(emoji, cancellationToken);
|
|
|
|
return await base.VisitEmojiAsync(emoji, cancellationToken);
|
|
|
@ -155,17 +177,21 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
|
|
|
{
|
|
|
|
{
|
|
|
|
if (mention.Kind == MentionKind.Everyone)
|
|
|
|
if (mention.Kind == MentionKind.Everyone)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
_buffer
|
|
|
|
_buffer.Append(
|
|
|
|
.Append("<span class=\"chatlog__markdown-mention\">")
|
|
|
|
// language=HTML
|
|
|
|
.Append("@everyone")
|
|
|
|
"""
|
|
|
|
.Append("</span>");
|
|
|
|
<span class="chatlog__markdown-mention">@everyone</span>
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (mention.Kind == MentionKind.Here)
|
|
|
|
else if (mention.Kind == MentionKind.Here)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
_buffer
|
|
|
|
_buffer.Append(
|
|
|
|
.Append("<span class=\"chatlog__markdown-mention\">")
|
|
|
|
// language=HTML
|
|
|
|
.Append("@here")
|
|
|
|
"""
|
|
|
|
.Append("</span>");
|
|
|
|
<span class="chatlog__markdown-mention">@here</span>
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (mention.Kind == MentionKind.User)
|
|
|
|
else if (mention.Kind == MentionKind.User)
|
|
|
|
{
|
|
|
|
{
|
|
|
@ -173,21 +199,25 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
|
|
|
var fullName = member?.User.FullName ?? "Unknown";
|
|
|
|
var fullName = member?.User.FullName ?? "Unknown";
|
|
|
|
var nick = member?.Nick ?? "Unknown";
|
|
|
|
var nick = member?.Nick ?? "Unknown";
|
|
|
|
|
|
|
|
|
|
|
|
_buffer
|
|
|
|
_buffer.Append(
|
|
|
|
.Append($"<span class=\"chatlog__markdown-mention\" title=\"{HtmlEncode(fullName)}\">")
|
|
|
|
// language=HTML
|
|
|
|
.Append('@').Append(HtmlEncode(nick))
|
|
|
|
$"""
|
|
|
|
.Append("</span>");
|
|
|
|
<span class="chatlog__markdown-mention" title="{HtmlEncode(fullName)}">@{HtmlEncode(nick)}</span>
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (mention.Kind == MentionKind.Channel)
|
|
|
|
else if (mention.Kind == MentionKind.Channel)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
var channel = mention.TargetId?.Pipe(_context.TryGetChannel);
|
|
|
|
var channel = mention.TargetId?.Pipe(_context.TryGetChannel);
|
|
|
|
var symbol = channel?.SupportsVoice == true ? "🔊" : "#";
|
|
|
|
var symbol = channel?.IsVoice == true ? "🔊" : "#";
|
|
|
|
var name = channel?.Name ?? "deleted-channel";
|
|
|
|
var name = channel?.Name ?? "deleted-channel";
|
|
|
|
|
|
|
|
|
|
|
|
_buffer
|
|
|
|
_buffer.Append(
|
|
|
|
.Append("<span class=\"chatlog__markdown-mention\">")
|
|
|
|
// language=HTML
|
|
|
|
.Append(symbol).Append(HtmlEncode(name))
|
|
|
|
$"""
|
|
|
|
.Append("</span>");
|
|
|
|
<span class="chatlog__markdown-mention">{symbol}{HtmlEncode(name)}</span>
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (mention.Kind == MentionKind.Role)
|
|
|
|
else if (mention.Kind == MentionKind.Role)
|
|
|
|
{
|
|
|
|
{
|
|
|
@ -196,38 +226,42 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
|
|
|
var color = role?.Color;
|
|
|
|
var color = role?.Color;
|
|
|
|
|
|
|
|
|
|
|
|
var style = color is not null
|
|
|
|
var style = color is not null
|
|
|
|
? $"color: rgb({color.Value.R}, {color.Value.G}, {color.Value.B}); " +
|
|
|
|
? $"""
|
|
|
|
$"background-color: rgba({color.Value.R}, {color.Value.G}, {color.Value.B}, 0.1);"
|
|
|
|
color: rgb({color.Value.R}, {color.Value.G}, {color.Value.B}); background-color: rgba({color.Value.R}, {color.Value.G}, {color.Value.B}, 0.1);
|
|
|
|
|
|
|
|
"""
|
|
|
|
: "";
|
|
|
|
: "";
|
|
|
|
|
|
|
|
|
|
|
|
_buffer
|
|
|
|
_buffer.Append(
|
|
|
|
.Append($"<span class=\"chatlog__markdown-mention\" style=\"{style}\">")
|
|
|
|
// language=HTML
|
|
|
|
.Append('@').Append(HtmlEncode(name))
|
|
|
|
$"""
|
|
|
|
.Append("</span>");
|
|
|
|
<span class="chatlog__markdown-mention" style="{style}">@{HtmlEncode(name)}</span>
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return await base.VisitMentionAsync(mention, cancellationToken);
|
|
|
|
return await base.VisitMentionAsync(mention, cancellationToken);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected override async ValueTask<MarkdownNode> VisitUnixTimestampAsync(
|
|
|
|
protected override async ValueTask<MarkdownNode> VisitTimestampAsync(
|
|
|
|
UnixTimestampNode timestamp,
|
|
|
|
TimestampNode timestamp,
|
|
|
|
CancellationToken cancellationToken = default)
|
|
|
|
CancellationToken cancellationToken = default)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
var dateString = timestamp.Date is not null
|
|
|
|
var formatted = timestamp.Instant is not null
|
|
|
|
? _context.FormatDate(timestamp.Date.Value)
|
|
|
|
? !string.IsNullOrWhiteSpace(timestamp.Format)
|
|
|
|
|
|
|
|
? timestamp.Instant.Value.ToLocalString(timestamp.Format)
|
|
|
|
|
|
|
|
: _context.FormatDate(timestamp.Instant.Value)
|
|
|
|
: "Invalid date";
|
|
|
|
: "Invalid date";
|
|
|
|
|
|
|
|
|
|
|
|
// Timestamp tooltips always use full date regardless of the configured format
|
|
|
|
var formattedLong = timestamp.Instant?.ToLocalString("dddd, MMMM d, yyyy h:mm tt") ?? "";
|
|
|
|
var longDateString = timestamp.Date is not null
|
|
|
|
|
|
|
|
? timestamp.Date.Value.ToLocalString("dddd, MMMM d, yyyy h:mm tt")
|
|
|
|
|
|
|
|
: "Invalid date";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_buffer
|
|
|
|
_buffer.Append(
|
|
|
|
.Append($"<span class=\"chatlog__markdown-timestamp\" title=\"{HtmlEncode(longDateString)}\">")
|
|
|
|
// language=HTML
|
|
|
|
.Append(HtmlEncode(dateString))
|
|
|
|
$"""
|
|
|
|
.Append("</span>");
|
|
|
|
<span class="chatlog__markdown-timestamp" title="{HtmlEncode(formattedLong)}">{HtmlEncode(formatted)}</span>
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return await base.VisitUnixTimestampAsync(timestamp, cancellationToken);
|
|
|
|
return await base.VisitTimestampAsync(timestamp, cancellationToken);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@ -248,9 +282,7 @@ internal partial class HtmlMarkdownVisitor
|
|
|
|
nodes.All(n => n is EmojiNode || n is TextNode textNode && string.IsNullOrWhiteSpace(textNode.Text));
|
|
|
|
nodes.All(n => n is EmojiNode || n is TextNode textNode && string.IsNullOrWhiteSpace(textNode.Text));
|
|
|
|
|
|
|
|
|
|
|
|
var buffer = new StringBuilder();
|
|
|
|
var buffer = new StringBuilder();
|
|
|
|
|
|
|
|
await new HtmlMarkdownVisitor(context, buffer, isJumbo).VisitAsync(nodes, cancellationToken);
|
|
|
|
await new HtmlMarkdownVisitor(context, buffer, isJumbo)
|
|
|
|
|
|
|
|
.VisitAsync(nodes, cancellationToken);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return buffer.ToString();
|
|
|
|
return buffer.ToString();
|
|
|
|
}
|
|
|
|
}
|
|
|
|