Update HTML layout and styles

pull/853/head
Oleksii Holub 3 years ago
parent 65cc3954b6
commit 5b4d88ca26

@ -1,60 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using DiscordChatExporter.Core.Discord.Data;
namespace DiscordChatExporter.Core.Exporting.Writers.Html;
// Used for grouping contiguous messages in HTML export
internal partial class MessageGroup
{
public User Author { get; }
public DateTimeOffset Timestamp { get; }
public IReadOnlyList<Message> Messages { get; }
public MessageReference? Reference { get; }
public Message? ReferencedMessage {get; }
public MessageGroup(
User author,
DateTimeOffset timestamp,
MessageReference? reference,
Message? referencedMessage,
IReadOnlyList<Message> messages)
{
Author = author;
Timestamp = timestamp;
Reference = reference;
ReferencedMessage = referencedMessage;
Messages = messages;
}
}
internal partial class MessageGroup
{
public static bool CanJoin(Message message1, Message message2) =>
// Must be from the same author
message1.Author.Id == message2.Author.Id &&
// Author's name must not have changed between messages
string.Equals(message1.Author.FullName, message2.Author.FullName, StringComparison.Ordinal) &&
// Duration between messages must be 7 minutes or less
(message2.Timestamp - message1.Timestamp).Duration().TotalMinutes <= 7 &&
// Other message must not be a reply
message2.Reference is null;
public static MessageGroup Join(IReadOnlyList<Message> messages)
{
var first = messages.First();
return new MessageGroup(
first.Author,
first.Timestamp,
first.Reference,
first.ReferencedMessage,
messages
);
}
}

@ -3,11 +3,14 @@
@using System.Threading.Tasks
@using DiscordChatExporter.Core.Discord.Data
@using DiscordChatExporter.Core.Exporting.Writers.Html;
@using DiscordChatExporter.Core.Utils.Extensions
@namespace DiscordChatExporter.Core.Exporting.Writers.Html
@inherits MiniRazor.TemplateBase<MessageGroupTemplateContext>
@{
var firstMessage = Model.Messages.First();
ValueTask<string> ResolveUrlAsync(string url) => Model.ExportContext.ResolveMediaUrlAsync(url);
string FormatDate(DateTimeOffset date) => Model.ExportContext.FormatDate(date);
@ -16,56 +19,78 @@
string FormatEmbedMarkdown(string markdown) => Model.FormatMarkdown(markdown, false);
var userMember = Model.ExportContext.TryGetMember(Model.MessageGroup.Author.Id);
var userColor = Model.ExportContext.TryGetUserColor(Model.MessageGroup.Author.Id);
var userColorStyle = userColor is not null
? $"color: rgb({userColor?.R},{userColor?.G},{userColor?.B})"
: null;
var userMember = Model.ExportContext.TryGetMember(firstMessage.Author.Id);
var userNick = Model.MessageGroup.Author.IsBot
? Model.MessageGroup.Author.Name
: userMember?.Nick ?? Model.MessageGroup.Author.Name;
var userColor = Model.ExportContext.TryGetUserColor(firstMessage.Author.Id);
var referencedUserMember = Model.MessageGroup.ReferencedMessage is not null
? Model.ExportContext.TryGetMember(Model.MessageGroup.ReferencedMessage.Author.Id)
: null;
var userNick = firstMessage.Author.IsBot
? firstMessage.Author.Name
: userMember?.Nick ?? firstMessage.Author.Name;
var referencedUserColor = Model.MessageGroup.ReferencedMessage is not null
? Model.ExportContext.TryGetUserColor(Model.MessageGroup.ReferencedMessage.Author.Id)
var referencedUserMember = firstMessage.ReferencedMessage is not null
? Model.ExportContext.TryGetMember(firstMessage.ReferencedMessage.Author.Id)
: null;
var referencedUserColorStyle = referencedUserColor is not null
? $"color: rgb({referencedUserColor?.R},{referencedUserColor?.G},{referencedUserColor?.B})"
var referencedUserColor = firstMessage.ReferencedMessage is not null
? Model.ExportContext.TryGetUserColor(firstMessage.ReferencedMessage.Author.Id)
: null;
var referencedUserNick = Model.MessageGroup.ReferencedMessage is not null
? Model.MessageGroup.ReferencedMessage.Author.IsBot
? Model.MessageGroup.ReferencedMessage.Author.Name
: referencedUserMember?.Nick ?? Model.MessageGroup.ReferencedMessage.Author.Name
var referencedUserNick = firstMessage.ReferencedMessage is not null
? firstMessage.ReferencedMessage.Author.IsBot
? firstMessage.ReferencedMessage.Author.Name
: referencedUserMember?.Nick ?? firstMessage.ReferencedMessage.Author.Name
: null;
}
<div class="chatlog__message-group">
@{/* Referenced message */}
@if (Model.MessageGroup.Reference is not null)
@for (var i = 0; i < Model.Messages.Count; i++)
{
var isFirst = i == 0;
var message = Model.Messages[i];
<div id="chatlog__message-container-@message.Id" class="chatlog__message-container @(message.IsPinned ? "chatlog__message-container--pinned" : null)" data-message-id="@message.Id">
<div class="chatlog__message">
@{/* Left side */}
<div class="chatlog__message-aside">
@if (isFirst)
{
// Reference symbol
if (message.Reference is not null)
{
<div class="chatlog__reference-symbol"></div>
}
// Avatar
<img class="chatlog__avatar" src="@await ResolveUrlAsync(message.Author.AvatarUrl)" alt="Avatar" loading="lazy">
}
else
{
<div class="chatlog__short-timestamp" title="@FormatDate(message.Timestamp)">@message.Timestamp.ToLocalString("t")</div>
}
</div>
@{/* Right side */}
<div class="chatlog__message-primary">
@if (isFirst)
{
// Reference
if (message.Reference is not null)
{
<div class="chatlog__reference">
@if (Model.MessageGroup.ReferencedMessage is not null)
@if (message.ReferencedMessage is not null)
{
<img class="chatlog__reference-avatar" src="@await ResolveUrlAsync(Model.MessageGroup.ReferencedMessage.Author.AvatarUrl)" alt="Avatar" loading="lazy">
<span class="chatlog__reference-name" title="@Model.MessageGroup.ReferencedMessage.Author.FullName" style="@referencedUserColorStyle">@referencedUserNick</span>
<img class="chatlog__reference-avatar" src="@await ResolveUrlAsync(message.ReferencedMessage.Author.AvatarUrl)" alt="Avatar" loading="lazy">
<div class="chatlog__reference-author" style="@(referencedUserColor is not null ? $"color: rgb({referencedUserColor.Value.R}, {referencedUserColor.Value.G}, {referencedUserColor.Value.B})" : null)" title="@message.ReferencedMessage.Author.FullName">@referencedUserNick</div>
<div class="chatlog__reference-content">
<span class="chatlog__reference-link" onclick="scrollToMessage(event, '@Model.MessageGroup.ReferencedMessage.Id')">
@if (!string.IsNullOrWhiteSpace(Model.MessageGroup.ReferencedMessage.Content))
<span class="chatlog__reference-link" onclick="scrollToMessage(event, '@message.ReferencedMessage.Id')">
@if (!string.IsNullOrWhiteSpace(message.ReferencedMessage.Content))
{
@Raw(FormatEmbedMarkdown(Model.MessageGroup.ReferencedMessage.Content))
@Raw(FormatEmbedMarkdown(message.ReferencedMessage.Content))
}
else if (Model.MessageGroup.ReferencedMessage.Attachments.Any() || Model.MessageGroup.ReferencedMessage.Embeds.Any())
else if (message.ReferencedMessage.Attachments.Any() || message.ReferencedMessage.Embeds.Any())
{
<em>Click to see attachment</em> <span>🖼️</span>
<em>Click to see attachment</em>
<span>🖼️</span>
}
else
{
@ -73,57 +98,50 @@
}
</span>
@if (Model.MessageGroup.ReferencedMessage.EditedTimestamp is not null)
@if (message.ReferencedMessage.EditedTimestamp is not null)
{
<span class="chatlog__reference-edited-timestamp" title="@FormatDate(Model.MessageGroup.ReferencedMessage.EditedTimestamp.Value)">(edited)</span>
<span class="chatlog__reference-edited-timestamp" title="@FormatDate(message.ReferencedMessage.EditedTimestamp.Value)">(edited)</span>
}
</div>
}
else
{
<span class="chatlog__reference-unknown">
<div class="chatlog__reference-unknown">
Original message was deleted or could not be loaded.
</span>
}
</div>
}
@{/* Avatar */}
<div class="chatlog__author-avatar-container">
<img class="chatlog__author-avatar" src="@await ResolveUrlAsync(Model.MessageGroup.Author.AvatarUrl)" alt="Avatar" loading="lazy">
</div>
}
<div class="chatlog__messages">
// Header
<div class="chatlog__header">
@{/* Author name */}
<span class="chatlog__author-name" title="@Model.MessageGroup.Author.FullName" data-user-id="@Model.MessageGroup.Author.Id" style="@userColorStyle">@userNick</span>
<span class="chatlog__author" style="@(userColor is not null ? $"color: rgb({userColor.Value.R}, {userColor.Value.G}, {userColor.Value.B})" : null)" title="@message.Author.FullName" data-user-id="@message.Author.Id">@userNick</span>
@{/* Bot tag */}
@if (Model.MessageGroup.Author.IsBot)
@{/* Bot label */}
@if (message.Author.IsBot)
{
<span class="chatlog__bot-tag">BOT</span>
<span class="chatlog__bot-label">BOT</span>
}
@{/* Message timestamp */}
<span class="chatlog__timestamp">@FormatDate(Model.MessageGroup.Timestamp)</span>
@{/* Timestamp */}
<span class="chatlog__timestamp">@FormatDate(message.Timestamp)</span>
</div>
}
@{/* Messages in a group */}
@foreach (var message in Model.MessageGroup.Messages)
{
<div class="chatlog__message @(message.IsPinned ? "chatlog__message--pinned" : null)" data-message-id="@message.Id" id="message-@message.Id" title="Message sent: @FormatDate(message.Timestamp)">
@{/* Content */}
@if (!string.IsNullOrWhiteSpace(message.Content) || message.EditedTimestamp is not null)
{
<div class="chatlog__content">
<div class="markdown">
@{/* Message content */}
<span class="preserve-whitespace">@Raw(FormatMarkdown(message.Content))</span>
<div class="chatlog__content chatlog__markdown">
@{/* Text */}
<span class="chatlog__markdown-preserve">@Raw(FormatMarkdown(message.Content))</span>
@{/* Edit timestamp */}
@{/* Edited timestamp */}
@if (message.EditedTimestamp is not null)
{
<span class="chatlog__edited-timestamp" title="@FormatDate(message.EditedTimestamp.Value)">(edited)</span>
}
</div>
</div>
}
@{/* Attachments */}
@ -159,7 +177,7 @@
{
<div class="chatlog__attachment-generic">
<svg class="chatlog__attachment-generic-icon">
<use href="#icon-attachment" />
<use href="#attachment-icon"/>
</svg>
<div class="chatlog__attachment-generic-name">
<a href="@await ResolveUrlAsync(attachment.Url)">
@ -202,7 +220,7 @@
@{/* Color pill */}
@if (embed.Color is not null)
{
<div class="chatlog__embed-color-pill" style="background-color: rgba(@embed.Color?.R,@embed.Color?.G,@embed.Color?.B,@embed.Color?.A)"></div>
<div class="chatlog__embed-color-pill" style="background-color: rgba(@embed.Color.Value.R, @embed.Color.Value.G, @embed.Color.Value.B, @embed.Color.Value.A)"></div>
}
else
{
@ -223,10 +241,10 @@
@if (!string.IsNullOrWhiteSpace(embed.Author.Name))
{
<span class="chatlog__embed-author-name">
<span class="chatlog__embed-author">
@if (!string.IsNullOrWhiteSpace(embed.Author.Url))
{
<a class="chatlog__embed-author-name-link" href="@embed.Author.Url">@embed.Author.Name</a>
<a class="chatlog__embed-author-link" href="@embed.Author.Url">@embed.Author.Name</a>
}
else
{
@ -244,12 +262,12 @@
@if (!string.IsNullOrWhiteSpace(embed.Url))
{
<a class="chatlog__embed-title-link" href="@embed.Url">
<div class="markdown preserve-whitespace">@Raw(FormatEmbedMarkdown(embed.Title))</div>
<div class="chatlog__markdown chatlog__markdown-preserve">@Raw(FormatEmbedMarkdown(embed.Title))</div>
</a>
}
else
{
<div class="markdown preserve-whitespace">@Raw(FormatEmbedMarkdown(embed.Title))</div>
<div class="chatlog__markdown chatlog__markdown-preserve">@Raw(FormatEmbedMarkdown(embed.Title))</div>
}
</div>
}
@ -270,7 +288,7 @@
@{/* Color pill */}
@if (embed.Color is not null)
{
<div class="chatlog__embed-color-pill" style="background-color: rgba(@embed.Color?.R,@embed.Color?.G,@embed.Color?.B,@embed.Color?.A)"></div>
<div class="chatlog__embed-color-pill" style="background-color: rgba(@embed.Color.Value.R, @embed.Color.Value.G, @embed.Color.Value.B, @embed.Color.Value.A)"></div>
}
else
{
@ -291,10 +309,10 @@
@if (!string.IsNullOrWhiteSpace(embed.Author.Name))
{
<span class="chatlog__embed-author-name">
<span class="chatlog__embed-author">
@if (!string.IsNullOrWhiteSpace(embed.Author.Url))
{
<a class="chatlog__embed-author-name-link" href="@embed.Author.Url">@embed.Author.Name</a>
<a class="chatlog__embed-author-link" href="@embed.Author.Url">@embed.Author.Name</a>
}
else
{
@ -312,12 +330,12 @@
@if (!string.IsNullOrWhiteSpace(embed.Url))
{
<a class="chatlog__embed-title-link" href="@embed.Url">
<div class="markdown preserve-whitespace">@Raw(FormatEmbedMarkdown(embed.Title))</div>
<div class="chatlog__markdown chatlog__markdown-preserve">@Raw(FormatEmbedMarkdown(embed.Title))</div>
</a>
}
else
{
<div class="markdown preserve-whitespace">@Raw(FormatEmbedMarkdown(embed.Title))</div>
<div class="chatlog__markdown chatlog__markdown-preserve">@Raw(FormatEmbedMarkdown(embed.Title))</div>
}
</div>
}
@ -326,7 +344,7 @@
@if (!string.IsNullOrWhiteSpace(embed.Description))
{
<div class="chatlog__embed-description">
<div class="markdown preserve-whitespace">@Raw(FormatEmbedMarkdown(embed.Description))</div>
<div class="chatlog__markdown chatlog__markdown-preserve">@Raw(FormatEmbedMarkdown(embed.Description))</div>
</div>
}
@ -340,14 +358,14 @@
@if (!string.IsNullOrWhiteSpace(field.Name))
{
<div class="chatlog__embed-field-name">
<div class="markdown preserve-whitespace">@Raw(FormatEmbedMarkdown(field.Name))</div>
<div class="chatlog__markdown chatlog__markdown-preserve">@Raw(FormatEmbedMarkdown(field.Name))</div>
</div>
}
@if (!string.IsNullOrWhiteSpace(field.Value))
{
<div class="chatlog__embed-field-value">
<div class="markdown preserve-whitespace">@Raw(FormatEmbedMarkdown(field.Value))</div>
<div class="chatlog__markdown chatlog__markdown-preserve">@Raw(FormatEmbedMarkdown(field.Value))</div>
</div>
}
</div>
@ -434,13 +452,14 @@
@foreach (var reaction in message.Reactions)
{
<div class="chatlog__reaction" title="@reaction.Emoji.Code">
<img class="emoji emoji--small" alt="@reaction.Emoji.Name" src="@await ResolveUrlAsync(reaction.Emoji.ImageUrl)" loading="lazy">
<img class="chatlog__emoji chatlog__emoji--small" alt="@reaction.Emoji.Name" src="@await ResolveUrlAsync(reaction.Emoji.ImageUrl)" loading="lazy">
<span class="chatlog__reaction-count">@reaction.Count</span>
</div>
}
</div>
}
</div>
}
</div>
</div>
}
</div>

@ -1,4 +1,6 @@
using DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors;
using System.Collections.Generic;
using DiscordChatExporter.Core.Discord.Data;
using DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors;
namespace DiscordChatExporter.Core.Exporting.Writers.Html;
@ -6,12 +8,12 @@ internal class MessageGroupTemplateContext
{
public ExportContext ExportContext { get; }
public MessageGroup MessageGroup { get; }
public IReadOnlyList<Message> Messages { get; }
public MessageGroupTemplateContext(ExportContext exportContext, MessageGroup messageGroup)
public MessageGroupTemplateContext(ExportContext exportContext, IReadOnlyList<Message> messages)
{
ExportContext = exportContext;
MessageGroup = messageGroup;
Messages = messages;
}
public string FormatMarkdown(string? markdown, bool isJumboAllowed = true) =>

@ -32,36 +32,38 @@
@{/* Styling */}
<style>
@@font-face {
font-family: Whitney;
src: url(@await ResolveUrlAsync(GetFontUrl(300)));
font-family: Whitney;
font-weight: 300;
}
@@font-face {
font-family: Whitney;
src: url(@await ResolveUrlAsync(GetFontUrl(400)));
font-family: Whitney;
font-weight: 400;
}
@@font-face {
font-family: Whitney;
src: url(@await ResolveUrlAsync(GetFontUrl(500)));
font-family: Whitney;
font-weight: 500;
}
@@font-face {
font-family: Whitney;
src: url(@await ResolveUrlAsync(GetFontUrl(600)));
font-family: Whitney;
font-weight: 600;
}
@@font-face {
font-family: Whitney;
src: url(@await ResolveUrlAsync(GetFontUrl(700)));
font-family: Whitney;
font-weight: 700;
}
body {
margin: 0;
padding: 0;
background-color: @Themed("#36393e", "#ffffff");
color: @Themed("#dcddde", "#23262a");
font-family: Whitney, "Helvetica Neue", Helvetica, Arial, sans-serif;
@ -80,180 +82,136 @@
img {
object-fit: contain;
image-rendering: high-quality;
image-rendering: -webkit-optimize-contrast;
}
.markdown {
.preamble {
display: grid;
grid-template-columns: auto 1fr;
max-width: 100%;
line-height: 1.3;
overflow-wrap: break-word;
padding: 0.6rem;
}
.preserve-whitespace {
white-space: pre-wrap;
}
.spoiler-text {
background-color: @Themed("rgba(255, 255, 255, 0.1)", "rgba(0, 0, 0, 0.1)");
border-radius: 3px;
}
.spoiler-text--hidden {
cursor: pointer;
background-color: @Themed("#202225", "#b9bbbe");
color: rgba(0, 0, 0, 0);
}
.spoiler-text--hidden:hover {
background-color: @Themed("rgba(32, 34, 37, 0.8)", "rgba(185, 187, 190, 0.8)");
}
.spoiler-text--hidden::selection {
color: rgba(0, 0, 0, 0);
}
.quote {
display: flex;
margin: 0.05em 0;
}
.quote-border {
margin-right: 0.5em;
border: 2px solid @Themed("#4f545c", "#c7ccd1");
border-radius: 3px;
}
.pre {
background-color: @Themed("#2f3136", "#f9f9f9");
font-family: "Consolas", "Courier New", Courier, monospace;
.preamble__guild-icon-container {
grid-column: 1;
}
.pre--multiline {
margin-top: 0.25em;
padding: 0.5em;
border: 2px solid @Themed("#282b30", "#f3f3f3");
border-radius: 5px;
color: @Themed("#b9bbbe", "#657b83");
.preamble__guild-icon {
max-width: 88px;
max-height: 88px;
}
@{/* Override Highlight.js styles with a higher specificity selector */}
.pre--multiline.hljs {
background-color: @Themed("#2f3136", "#f9f9f9");
color: @Themed("#b9bbbe", "#657b83");
.preamble__entries-container {
grid-column: 2;
margin-left: 0.6rem;
}
.pre--inline {
padding: 2px;
border-radius: 3px;
font-size: 0.85em;
.preamble__entry {
margin-bottom: 0.15rem;
color: @Themed("#ffffff", "#2f3136");
font-size: 1.4rem;
}
.mention {
border-radius: 3px;
padding: 0 2px;
color: @Themed("#dee0fc", "#505cdc");
background-color: @Themed("rgba(88, 101, 242, .3)", "rgba(88, 101, 242, .15)");
font-weight: 500;
.preamble__entry--small {
font-size: 1rem;
}
.mention:hover {
background-color: #5865f2;
color: #ffffff
.chatlog {
margin-top: 0.6rem;
width: 100%;
border-top: 1px solid @Themed("rgba(255, 255, 255, 0.1)", "#eceeef");
border-bottom: 1px solid @Themed("rgba(255, 255, 255, 0.1)", "#eceeef");
}
.timestamp {
border-radius: 3px;
padding: 0 2px;
background-color: @Themed("rgba(255, 255, 255, 0.06)", "rgba(6, 6, 7, 0.08)");
.chatlog__message-group {
margin-bottom: 1rem;
}
.emoji {
width: 1.325em;
height: 1.325em;
margin: 0 0.06em;
vertical-align: -0.4em;
.chatlog__message-container {
background-color: transparent;
transition: background-color 1s ease;
}
.emoji--small {
width: 1em;
height: 1em;
.chatlog__message-container--highlighted {
background-color: @Themed("rgba(114, 137, 218, 0.2)", "rgba(114, 137, 218, 0.2)");
}
.emoji--large {
width: 2.8em;
height: 2.8em;
.chatlog__message-container--pinned {
background-color: @Themed("rgba(249, 168, 37, 0.05)", "rgba(249, 168, 37, 0.05)");
}
.preamble {
.chatlog__message {
display: grid;
margin: 0 0.3em 0.6em 0.3em;
max-width: 100%;
grid-template-columns: auto 1fr;
padding: 0.25rem 0;
direction: ltr;
unicode-bidi: bidi-override;
}
.preamble__guild-icon-container {
grid-column: 1;
.chatlog__message:hover {
background-color: @Themed("#32353b", "#fafafa");
}
.preamble__guild-icon {
max-width: 88px;
max-height: 88px;
}
.preamble__entries-container {
grid-column: 2;
margin-left: 0.6em;
.chatlog__message:hover .chatlog__short-timestamp {
display: block;
}
.preamble__entry {
font-size: 1.4em;
color: @Themed("#ffffff", "#2f3136");
.chatlog__message-aside {
grid-column: 1;
width: 72px;
padding: 0.15rem 0.15rem 0 0.15rem;
text-align: center;
}
.preamble__entry--small {
font-size: 1em;
.chatlog__reference-symbol {
height: 12px;
margin: 4px 4px 4px 36px;
border-left: 2px solid @Themed("#4f545c", "#c7ccd1");
border-top: 2px solid @Themed("#4f545c", "#c7ccd1");
border-radius: 8px 0 0 0;
}
.chatlog {
max-width: 100%;
.chatlog__avatar {
width: 40px;
height: 40px;
border-radius: 50%;
}
.chatlog__message-group {
display: grid;
margin: 0 0.6em;
padding: 0.9em 0;
border-top: 1px solid @Themed("rgba(255, 255, 255, 0.1)", "#eceeef");
grid-template-columns: auto 1fr;
.chatlog__short-timestamp {
display: none;
color: @Themed("#a3a6aa", "#5e6772");
font-size: 0.75rem;
font-weight: 500;
direction: ltr;
unicode-bidi: bidi-override;
}
.chatlog__reference-symbol {
grid-column: 1;
margin: 8px 4px 4px 18px;
border-left: 2px solid @Themed("#4f545c", "#c7ccd1");
border-top: 2px solid @Themed("#4f545c", "#c7ccd1");
border-radius: 8px 0 0 0;
.chatlog__message-primary {
grid-column: 2;
min-width: 0;
}
.chatlog__reference {
display: flex;
grid-column: 2;
margin-bottom: 0.25em;
font-size: 0.875em;
margin-bottom: 0.15rem;
align-items: center;
color: @Themed("#b5b6b8", "#5f5f60");
font-size: 0.875rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
align-items: center;
color: @Themed("#b5b6b8", "#5f5f60");
}
.chatlog__reference-avatar {
border-radius: 50%;
width: 16px;
height: 16px;
margin-right: 0.25em;
margin-right: 0.25rem;
border-radius: 50%;
}
.chatlog__reference-name {
margin-right: 0.3em;
.chatlog__reference-author {
margin-right: 0.3rem;
font-weight: 600;
}
@ -262,13 +220,8 @@
text-overflow: ellipsis;
}
.chatlog__reference-content a:hover {
text-decoration: none;
}
.chatlog__reference-link {
cursor: pointer;
color: @Themed("#b5b6b8", "#5f5f60");
}
.chatlog__reference-link * {
@ -276,83 +229,75 @@
pointer-events: none;
}
.chatlog__reference-link .hljs {
display: inline;
}
.chatlog__reference-link:hover {
color: @Themed("#ffffff", "#2f3136");
}
.chatlog__reference-link:hover *:not(.spoiler-text) {
color: @Themed("#ffffff", "#2f3136");
.chatlog__reference-link:hover *:not(.chatlog__markdown-spoiler) {
color: inherit;
}
.chatlog__reference-edited-timestamp {
margin-left: 0.25em;
font-size: 0.8em;
margin-left: 0.25rem;
color: @Themed("#a3a6aa", "#5e6772");
font-size: 0.75rem;
font-weight: 500;
direction: ltr;
unicode-bidi: bidi-override;
color: @Themed("rgba(255, 255, 255, 0.2)", "#747f8d");
}
.chatlog__author-avatar-container {
grid-column: 1;
width: 40px;
height: 40px;
margin-right: 16px;
}
.chatlog__author-avatar {
border-radius: 50%;
width: 40px;
height: 40px;
image-rendering: high-quality;
}
.chatlog__messages {
grid-column: 2;
min-width: 50%;
direction: ltr;
.chatlog__header {
margin-bottom: 0.25rem;
}
.chatlog__author-name {
.chatlog__author {
font-weight: @Themed("500", "600");
color: @Themed("#ffffff", "#2f3136");
}
.chatlog__bot-label {
position: relative;
top: -.1rem;
margin-left: 0.3rem;
padding: 0.05rem 0.3rem;
border-radius: 3px;
background-color: #5865F2;
color: #ffffff;
font-size: 0.625rem;
font-weight: 500;
line-height: 1.3;
}
.chatlog__timestamp {
margin-left: 0.3em;
font-size: 0.75em;
margin-left: 0.3rem;
color: @Themed("#a3a6aa", "#5e6772");
font-size: 0.75rem;
font-weight: 500;
direction: ltr;
unicode-bidi: bidi-override;
color: @Themed("rgba(255, 255, 255, 0.2)", "#747f8d");
}
.chatlog__message {
padding: 0.1em 0.3em;
margin: 0 -0.3em;
background-color: transparent;
transition: background-color 1s ease;
}
.chatlog__message--highlighted {
background-color: @Themed("rgba(114, 137, 218, 0.2)", "rgba(114, 137, 218, 0.2)");
}
.chatlog__message--pinned {
background-color: @Themed("rgba(249, 168, 37, 0.05)", "rgba(249, 168, 37, 0.05)");
}
.chatlog__content {
font-size: 0.95em;
padding-right: 1rem;
font-size: 0.95rem;
word-wrap: break-word;
}
.chatlog__edited-timestamp {
margin-left: 0.15em;
font-size: 0.8em;
margin-left: 0.15rem;
color: @Themed("#a3a6aa", "#5e6772");
font-size: 0.75rem;
font-weight: 500;
}
.chatlog__attachment {
display: inline-block;
position: relative;
margin-top: 0.3em;
width: fit-content;
margin-top: 0.3rem;
border-radius: 3px;
overflow: hidden;
}
@ -368,18 +313,18 @@
.chatlog__attachment-spoiler-caption {
display: none;
z-index: 999;
position: absolute;
left: 50%;
top: 50%;
z-index: 999;
padding: 0.4rem 0.8rem;
border-radius: 20px;
transform: translate(-50%, -50%);
padding: 0.4em 0.8em;
background-color: rgba(0, 0, 0, 0.9);
border-radius: 20px;
color: #dcddde;
font-size: 0.9em;
font-size: 0.9rem;
font-weight: 600;
letter-spacing: 0.05em;
letter-spacing: 0.05rem;
}
.chatlog__attachment--hidden .chatlog__attachment-spoiler-caption {
@ -391,9 +336,9 @@
}
.chatlog__attachment-media {
vertical-align: top;
max-width: 45vw;
max-height: 500px;
vertical-align: top;
border-radius: 3px;
}
@ -402,13 +347,13 @@
}
.chatlog__attachment-generic {
width: 100%;
max-width: 520px;
width: 100%;
height: 40px;
padding: 10px;
background-color: @Themed("#2f3136", "#f2f3f5");
border: 1px solid @Themed("#292b2f", "#ebedef");
border-radius: 3px;
background-color: @Themed("#2f3136", "#f2f3f5");
overflow: hidden;
}
@ -434,19 +379,15 @@
text-overflow: ellipsis;
}
.chatlog__edited-timestamp {
color: @Themed("rgba(255, 255, 255, 0.2)", "#747f8d");
}
.chatlog__embed {
display: flex;
margin-top: 0.3em;
margin-top: 0.3rem;
max-width: 520px;
}
.chatlog__embed-color-pill {
flex-shrink: 0;
width: 0.25em;
width: 0.25rem;
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
@ -458,11 +399,11 @@
.chatlog__embed-content-container {
display: flex;
flex-direction: column;
padding: 0.5em 0.6em;
background-color: @Themed("rgba(46, 48, 54, 0.3)", "rgba(249, 249, 249, 0.3)");
padding: 0.5rem 0.6rem;
border: 1px solid @Themed("rgba(46, 48, 54, 0.6)", "rgba(204, 204, 204, 0.3)");
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
background-color: @Themed("rgba(46, 48, 54, 0.3)", "rgba(249, 249, 249, 0.3)");
}
.chatlog__embed-content {
@ -476,83 +417,83 @@
.chatlog__embed-author {
display: flex;
margin-bottom: 0.3em;
margin-bottom: 0.3rem;
align-items: center;
}
.chatlog__embed-author-icon {
margin-right: 0.5em;
width: 20px;
height: 20px;
margin-right: 0.5rem;
border-radius: 50%;
}
.chatlog__embed-author-name {
font-size: 0.875em;
.chatlog__embed-author {
color: @Themed("#ffffff", "#4f545c")
font-size: 0.875rem;
font-weight: 600;
direction: ltr;
unicode-bidi: bidi-override;
color: @Themed("#ffffff", "#4f545c")
}
.chatlog__embed-author-name-link {
.chatlog__embed-author-link {
color: @Themed("#ffffff", "#4f545c");
}
.chatlog__embed-title {
margin-bottom: 0.2em;
font-size: 0.875em;
font-weight: 600;
margin-bottom: 0.75rem;
color: @Themed("#ffffff", "#4f545c");
font-size: 0.875rem;
font-weight: 600;
}
.chatlog__embed-description {
font-weight: 500;
font-size: 0.85em;
color: @Themed("#dcddde", "#2e3338");
font-weight: 500;
font-size: 0.85rem;
}
.chatlog__embed-fields {
display: flex;
flex-wrap: wrap;
gap: 0 0.5em;
gap: 0 0.5rem;
}
.chatlog__embed-field {
flex: 0;
min-width: 100%;
max-width: 506px;
padding-top: 0.6em;
font-size: 0.875em;
padding-top: 0.6rem;
font-size: 0.875rem;
}
.chatlog__embed-field--inline {
flex: 1;
flex-basis: auto;
min-width: 150px;
min-width: 50px;
}
.chatlog__embed-field-name {
margin-bottom: 0.2em;
font-weight: 600;
margin-bottom: 0.2rem;
color: @Themed("#ffffff", "#36393e");
font-weight: 600;
}
.chatlog__embed-field-value {
font-weight: 500;
color: @Themed("#dcddde", "#2e3338");
font-weight: 500;
}
.chatlog__embed-thumbnail {
flex: 0;
margin-left: 1.2em;
max-width: 80px;
max-height: 80px;
margin-left: 1.2rem;
border-radius: 3px;
}
.chatlog__embed-image-container {
margin-top: 0.6em;
margin-top: 0.6rem;
}
.chatlog__embed-image {
@ -562,28 +503,28 @@
}
.chatlog__embed-footer {
margin-top: 0.6em;
margin-top: 0.6rem;
color: @Themed("#dcddde", "#2e3338");
}
.chatlog__embed-footer-icon {
margin-right: 0.2em;
width: 20px;
height: 20px;
margin-right: 0.2rem;
border-radius: 50%;
vertical-align: middle;
}
.chatlog__embed-footer-text {
vertical-align: middle;
font-size: 0.75em;
font-size: 0.75rem;
font-weight: 500;
}
.chatlog__embed-plainimage {
vertical-align: top;
max-width: 45vw;
max-height: 500px;
vertical-align: top;
border-radius: 3px;
}
@ -592,7 +533,7 @@
}
.chatlog__embed-youtube-container {
margin-top: 0.6em;
margin-top: 0.6rem;
}
.chatlog__embed-youtube {
@ -616,51 +557,132 @@
.chatlog__reaction {
display: flex;
align-items: center;
margin: 0.35em 0.1em 0.1em 0;
padding: .125rem .375rem;
background-color: @Themed("#2f3136", "#f2f3f5");
margin: 0.35rem 0.1rem 0.1rem 0;
padding: 0.125rem 0.375rem;
border: 1px solid transparent;
border-radius: 8px;
border-color: transparent;
border-width: 1px;
border-style: solid;
background-color: @Themed("#2f3136", "#f2f3f5");
align-items: center;
}
.chatlog__reaction:hover {
border-color: @Themed("hsla(0,0%,100%,.2)", "rgba(0, 0, 0, 0.2)");
border-width: 1px;
border-style: solid;
border: 1px solid @Themed("hsla(0,0%,100%,.2)", "rgba(0, 0, 0, 0.2)");
background-color: @Themed("transparent", "white");
}
.chatlog__reaction-count {
min-width: 9px;
margin-left: 0.35em;
font-size: 0.875em;
margin-left: 0.35rem;
color: @Themed("#b9bbbe", "#4f5660");
font-size: 0.875rem;
}
.chatlog__reaction:hover .chatlog__reaction-count {
color: @Themed("#dcddde", "#2e3338");
}
.chatlog__bot-tag {
position: relative;
top: -.1em;
margin-left: 0.3em;
padding: 0.05em 0.3em;
border-radius: 3px;
.chatlog__markdown {
max-width: 100%;
line-height: 1.3;
background-color: #5865F2;
color: #ffffff;
font-size: 0.625em;
overflow-wrap: break-word;
}
.chatlog__markdown-preserve {
white-space: pre-wrap;
}
.chatlog__markdown-spoiler {
background-color: @Themed("rgba(255, 255, 255, 0.1)", "rgba(0, 0, 0, 0.1)");
border-radius: 3px;
}
.chatlog__markdown-spoiler--hidden {
cursor: pointer;
background-color: @Themed("#202225", "#b9bbbe");
color: rgba(0, 0, 0, 0);
}
.chatlog__markdown-spoiler--hidden:hover {
background-color: @Themed("rgba(32, 34, 37, 0.8)", "rgba(185, 187, 190, 0.8)");
}
.chatlog__markdown-spoiler--hidden::selection {
color: rgba(0, 0, 0, 0);
}
.chatlog__markdown-quote {
display: flex;
margin: 0.05rem 0;
}
.chatlog__markdown-quote-border {
margin-right: 0.5rem;
border: 2px solid @Themed("#4f545c", "#c7ccd1");
border-radius: 3px;
}
.chatlog__markdown-pre {
background-color: @Themed("#2f3136", "#f9f9f9");
font-family: "Consolas", "Courier New", Courier, monospace;
}
.chatlog__markdown-pre--multiline {
margin-top: 0.25rem;
padding: 0.5rem;
border: 2px solid @Themed("#282b30", "#f3f3f3");
border-radius: 5px;
color: @Themed("#b9bbbe", "#657b83");
}
.chatlog__markdown-pre--multiline.hljs {
background-color: inherit;
color: inherit;
}
.chatlog__markdown-pre--inline {
padding: 2px;
border-radius: 3px;
font-size: 0.85rem;
}
.chatlog__markdown-mention{
border-radius: 3px;
padding: 0 2px;
background-color: @Themed("rgba(88, 101, 242, .3)", "rgba(88, 101, 242, .15)");
color: @Themed("#dee0fc", "#505cdc");
font-weight: 500;
}
.chatlog__markdown-mention:hover {
background-color: #5865f2;
color: #ffffff
}
.chatlog__markdown-timestamp{
border-radius: 3px;
padding: 0 2px;
color: @Themed("#a3a6aa", "#5e6772");
}
.chatlog__emoji {
width: 1.325rem;
height: 1.325rem;
margin: 0 0.06rem;
vertical-align: -0.4rem;
}
.chatlog__emoji--small {
width: 1rem;
height: 1rem;
}
.chatlog__emoji--large {
width: 2.8rem;
height: 2.8rem;
}
.postamble {
margin: 1.4em 0.3em 0.6em 0.3em;
padding: 1em;
border-top: 1px solid @Themed("rgba(255, 255, 255, 0.1)", "#eceeef");
padding: 1.25rem;
}
.postamble__entry {
@ -669,16 +691,16 @@
</style>
@{/* Syntax highlighting */}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/solarized-@(Model.ThemeName.ToLowerInvariant()).min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js"></script>
<link rel="stylesheet" href="@ResolveUrlAsync($"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/solarized-{Model.ThemeName.ToLowerInvariant()}.min.css")">
<script src="@ResolveUrlAsync("https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js")"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.pre--multiline').forEach(e => hljs.highlightBlock(e));
document.querySelectorAll('.chatlog__markdown-pre--multiline').forEach(e => hljs.highlightBlock(e));
});
</script>
@{/* Lottie animation support */}
<script src="https://cdnjs.cloudflare.com/ajax/libs/lottie-web/5.8.1/lottie.min.js"></script>
<script src="@ResolveUrlAsync("https://cdnjs.cloudflare.com/ajax/libs/lottie-web/5.8.1/lottie.min.js")"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.chatlog__sticker--media[data-source]').forEach(e => {
@ -702,12 +724,12 @@
@{/* Scripts */}
<script>
function scrollToMessage(event, id) {
var element = document.getElementById('message-' + id);
var element = document.getElementById('chatlog__message-container-' + id);
if (!element)
return;
if (element) {
event.preventDefault();
element.classList.add('chatlog__message--highlighted');
element.classList.add('chatlog__message-container--highlighted');
window.scrollTo({
top: element.getBoundingClientRect().top - document.body.getBoundingClientRect().top - (window.innerHeight / 2),
@ -715,26 +737,29 @@
});
window.setTimeout(function() {
element.classList.remove('chatlog__message--highlighted');
element.classList.remove('chatlog__message-container--highlighted');
}, 2000);
}
}
function showSpoiler(event, element) {
if (element && element.classList.contains('spoiler-text--hidden')) {
if (!element)
return;
if (element.classList.contains('chatlog__attachment--hidden')) {
event.preventDefault();
element.classList.remove('spoiler-text--hidden');
element.classList.remove('chatlog__attachment--hidden');
}
if (element && element.classList.contains('chatlog__attachment--hidden')) {
if (element.classList.contains('chatlog__markdown-spoiler--hidden')) {
event.preventDefault();
element.classList.remove('chatlog__attachment--hidden');
element.classList.remove('chatlog__markdown-spoiler--hidden');
}
}
</script>
@{/* Icons */}
<svg style="display: none">
<symbol id="icon-attachment" viewBox="0 0 720 960">
<symbol id="attachment-icon" viewBox="0 0 720 960">
<path fill="#f4f5fb" d="M50,935a25,25,0,0,1-25-25V50A25,25,0,0,1,50,25H519.6L695,201.32V910a25,25,0,0,1-25,25Z"/>
<path fill="#7789c4" d="M509.21,50,670,211.63V910H50V50H509.21M530,0H50A50,50,0,0,0,0,50V910a50,50,0,0,0,50,50H670a50,50,0,0,0,50-50h0V191Z"/>
<path fill="#f4f5fb" d="M530,215a25,25,0,0,1-25-25V50a25,25,0,0,1,16.23-23.41L693.41,198.77A25,25,0,0,1,670,215Z"/>

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
@ -13,7 +14,7 @@ internal class HtmlMessageWriter : MessageWriter
private readonly TextWriter _writer;
private readonly string _themeName;
private readonly List<Message> _messageGroupBuffer = new();
private readonly List<Message> _messageGroup = new();
public HtmlMessageWriter(Stream stream, ExportContext context, string themeName)
: base(stream, context)
@ -22,6 +23,23 @@ internal class HtmlMessageWriter : MessageWriter
_themeName = themeName;
}
private bool CanJoinGroup(Message message)
{
var lastMessage = _messageGroup.LastOrDefault();
if (lastMessage is null)
return true;
return
// Must be from the same author
lastMessage.Author.Id == message.Author.Id &&
// Author's name must not have changed between messages
string.Equals(lastMessage.Author.FullName, message.Author.FullName, StringComparison.Ordinal) &&
// Duration between messages must be 7 minutes or less
(message.Timestamp - lastMessage.Timestamp).Duration().TotalMinutes <= 7 &&
// Other message must not be a reply
message.Reference is null;
}
public override async ValueTask WritePreambleAsync(CancellationToken cancellationToken = default)
{
var templateContext = new PreambleTemplateContext(Context, _themeName);
@ -34,10 +52,10 @@ internal class HtmlMessageWriter : MessageWriter
}
private async ValueTask WriteMessageGroupAsync(
MessageGroup messageGroup,
IReadOnlyList<Message> messages,
CancellationToken cancellationToken = default)
{
var templateContext = new MessageGroupTemplateContext(Context, messageGroup);
var templateContext = new MessageGroupTemplateContext(Context, messages);
// We are not writing directly to output because Razor
// does not actually do asynchronous writes to stream.
@ -52,31 +70,26 @@ internal class HtmlMessageWriter : MessageWriter
{
await base.WriteMessageAsync(message, cancellationToken);
// If message group is empty or the given message can be grouped, buffer the given message
if (!_messageGroupBuffer.Any() || MessageGroup.CanJoin(_messageGroupBuffer.Last(), message))
// If the message can be grouped, buffer it for now
if (CanJoinGroup( message))
{
_messageGroupBuffer.Add(message);
_messageGroup.Add(message);
}
// Otherwise, flush the group and render messages
else
{
await WriteMessageGroupAsync(MessageGroup.Join(_messageGroupBuffer), cancellationToken);
await WriteMessageGroupAsync(_messageGroup, cancellationToken);
_messageGroupBuffer.Clear();
_messageGroupBuffer.Add(message);
_messageGroup.Clear();
_messageGroup.Add(message);
}
}
public override async ValueTask WritePostambleAsync(CancellationToken cancellationToken = default)
{
// Flush current message group
if (_messageGroupBuffer.Any())
{
await WriteMessageGroupAsync(
MessageGroup.Join(_messageGroupBuffer),
cancellationToken
);
}
if (_messageGroup.Any())
await WriteMessageGroupAsync(_messageGroup, cancellationToken);
var templateContext = new PostambleTemplateContext(Context, MessagesWritten);

@ -33,15 +33,37 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
{
var (tagOpen, tagClose) = formatting.Kind switch
{
FormattingKind.Bold => ("<strong>", "</strong>"),
FormattingKind.Italic => ("<em>", "</em>"),
FormattingKind.Underline => ("<u>", "</u>"),
FormattingKind.Strikethrough => ("<s>", "</s>"),
FormattingKind.Bold => (
"<strong>",
"</strong>"
),
FormattingKind.Italic => (
"<em>",
"</em>"
),
FormattingKind.Underline => (
"<u>",
"</u>"
),
FormattingKind.Strikethrough => (
"<s>",
"</s>"
),
FormattingKind.Spoiler => (
"<span class=\"spoiler-text spoiler-text--hidden\" onclick=\"showSpoiler(event, this)\">", "</span>"),
"<span class=\"chatlog__markdown-spoiler chatlog__markdown-spoiler--hidden\" onclick=\"showSpoiler(event, this)\">",
"</span>"
),
FormattingKind.Quote => (
"<div class=\"quote\"><div class=\"quote-border\"></div><div class=\"quote-content\">", "</div></div>"),
_ => throw new ArgumentOutOfRangeException(nameof(formatting.Kind))
"<div class=\"chatlog__markdown-quote\"><div class=\"chatlog__markdown-quote-border\"></div><div class=\"chatlog__markdown-quote-content\">",
"</div></div>"
),
_ => throw new InvalidOperationException($"Unknown formatting kind '{formatting.Kind}'.")
};
_buffer.Append(tagOpen);
@ -54,7 +76,7 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
protected override MarkdownNode VisitInlineCodeBlock(InlineCodeBlockNode inlineCodeBlock)
{
_buffer
.Append("<span class=\"pre pre--inline\">")
.Append("<span class=\"chatlog__markdown-pre chatlog__markdown-pre--inline\">")
.Append(HtmlEncode(inlineCodeBlock.Code))
.Append("</span>");
@ -68,7 +90,7 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
: "nohighlight";
_buffer
.Append($"<div class=\"pre pre--multiline {highlightCssClass}\">")
.Append($"<div class=\"chatlog__markdown-pre chatlog__markdown-pre--multiline {highlightCssClass}\">")
.Append(HtmlEncode(multiLineCodeBlock.Code))
.Append("</div>");
@ -98,10 +120,10 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
protected override MarkdownNode VisitEmoji(EmojiNode emoji)
{
var emojiImageUrl = Emoji.GetImageUrl(emoji.Id, emoji.Name, emoji.IsAnimated);
var jumboClass = _isJumbo ? "emoji--large" : "";
var jumboClass = _isJumbo ? "chatlog__emoji--large" : "";
_buffer
.Append($"<img loading=\"lazy\" class=\"emoji {jumboClass}\" alt=\"{emoji.Name}\" title=\"{emoji.Code}\" src=\"{emojiImageUrl}\">");
.Append($"<img loading=\"lazy\" class=\"chatlog__emoji {jumboClass}\" alt=\"{emoji.Name}\" title=\"{emoji.Code}\" src=\"{emojiImageUrl}\">");
return base.VisitEmoji(emoji);
}
@ -111,14 +133,14 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
if (mention.Kind == MentionKind.Everyone)
{
_buffer
.Append("<span class=\"mention\">")
.Append("<span class=\"chatlog__markdown-mention\">")
.Append("@everyone")
.Append("</span>");
}
else if (mention.Kind == MentionKind.Here)
{
_buffer
.Append("<span class=\"mention\">")
.Append("<span class=\"chatlog__markdown-mention\">")
.Append("@here")
.Append("</span>");
}
@ -129,7 +151,7 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
var nick = member?.Nick ?? "Unknown";
_buffer
.Append($"<span class=\"mention\" title=\"{HtmlEncode(fullName)}\">")
.Append($"<span class=\"chatlog__markdown-mention\" title=\"{HtmlEncode(fullName)}\">")
.Append('@').Append(HtmlEncode(nick))
.Append("</span>");
}
@ -140,7 +162,7 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
var name = channel?.Name ?? "deleted-channel";
_buffer
.Append("<span class=\"mention\">")
.Append("<span class=\"chatlog__markdown-mention\">")
.Append(symbol).Append(HtmlEncode(name))
.Append("</span>");
}
@ -151,11 +173,12 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
var color = role?.Color;
var style = color is not null
? $"color: rgb({color?.R}, {color?.G}, {color?.B}); background-color: rgba({color?.R}, {color?.G}, {color?.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
.Append($"<span class=\"mention\" style=\"{style}\">")
.Append($"<span class=\"chatlog__markdown-mention\" style=\"{style}\">")
.Append('@').Append(HtmlEncode(name))
.Append("</span>");
}
@ -175,7 +198,7 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
: "Invalid date";
_buffer
.Append($"<span class=\"timestamp\" title=\"{HtmlEncode(longDateString)}\">")
.Append($"<span class=\"chatlog__markdown-timestamp\" title=\"{HtmlEncode(longDateString)}\">")
.Append(HtmlEncode(dateString))
.Append("</span>");

@ -77,8 +77,9 @@ internal static partial class MarkdownParser
// Capture any character until the end of the line
// Opening 'greater than' character must be followed by whitespace
// Text content is optional
private static readonly IMatcher<MarkdownNode> SingleLineQuoteNodeMatcher = new RegexMatcher<MarkdownNode>(
new Regex("^>\\s(.+\n?)", DefaultRegexOptions),
new Regex("^>\\s(.*\n?)", DefaultRegexOptions),
(s, m) => new FormattingNode(FormattingKind.Quote, Parse(s.Relocate(m.Groups[1])))
);
@ -86,7 +87,7 @@ internal static partial class MarkdownParser
// This one is tricky as it ends up producing multiple separate captures which need to be joined
private static readonly IMatcher<MarkdownNode> RepeatedSingleLineQuoteNodeMatcher =
new RegexMatcher<MarkdownNode>(
new Regex("(?:^>\\s(.+\n?)){2,}", DefaultRegexOptions),
new Regex("(?:^>\\s(.*\n?)){2,}", DefaultRegexOptions),
(_, m) =>
{
var content = string.Concat(m.Groups[1].Captures.Select(c => c.Value));

Loading…
Cancel
Save