using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading.Tasks; using DiscordChatExporter.Core.Models; using DiscordChatExporter.Core.Rendering.Logic; using Scriban; using Scriban.Runtime; using Tyrrrz.Extensions; namespace DiscordChatExporter.Core.Rendering { public partial class HtmlMessageRenderer : MessageRendererBase { private readonly string _themeName; private readonly List _messageGroupBuffer = new List(); private readonly Template _leadingBlockTemplate; private readonly Template _messageGroupTemplate; private readonly Template _trailingBlockTemplate; private bool _isLeadingBlockRendered; public HtmlMessageRenderer(string filePath, RenderContext context, string themeName) : base(filePath, context) { _themeName = themeName; _leadingBlockTemplate = Template.Parse(GetLeadingBlockTemplateCode()); _messageGroupTemplate = Template.Parse(GetMessageGroupTemplateCode()); _trailingBlockTemplate = Template.Parse(GetTrailingBlockTemplateCode()); } private MessageGroup GetCurrentMessageGroup() { var firstMessage = _messageGroupBuffer.First(); return new MessageGroup(firstMessage.Author, firstMessage.Timestamp, _messageGroupBuffer); } private TemplateContext CreateTemplateContext(IReadOnlyDictionary? constants = null) { // Template context var templateContext = new TemplateContext { MemberRenamer = m => m.Name, MemberFilter = m => true, LoopLimit = int.MaxValue, StrictVariables = true }; // Model var scriptObject = new ScriptObject(); // Constants scriptObject.SetValue("Context", Context, true); scriptObject.SetValue("CoreStyleSheet", GetCoreStyleSheetCode(), true); scriptObject.SetValue("ThemeStyleSheet", GetThemeStyleSheetCode(_themeName), true); scriptObject.SetValue("HighlightJsStyleName", $"solarized-{_themeName.ToLowerInvariant()}", true); // Additional constants if (constants != null) { foreach (var (member, value) in constants) scriptObject.SetValue(member, value, true); } // Functions scriptObject.Import("FormatDate", new Func(d => SharedRenderingLogic.FormatDate(d, Context.DateFormat))); scriptObject.Import("FormatMarkdown", new Func(m => HtmlRenderingLogic.FormatMarkdown(Context, m))); // Push model templateContext.PushGlobal(scriptObject); // Push output templateContext.PushOutput(new TextWriterOutput(Writer)); return templateContext; } private async Task RenderLeadingBlockAsync() { var templateContext = CreateTemplateContext(); await templateContext.EvaluateAsync(_leadingBlockTemplate.Page); } private async Task RenderCurrentMessageGroupAsync() { var templateContext = CreateTemplateContext(new Dictionary { ["MessageGroup"] = GetCurrentMessageGroup() }); await templateContext.EvaluateAsync(_messageGroupTemplate.Page); } private async Task RenderTrailingBlockAsync() { var templateContext = CreateTemplateContext(); await templateContext.EvaluateAsync(_trailingBlockTemplate.Page); } public override async Task RenderMessageAsync(Message message) { // Render leading block if it's the first entry if (!_isLeadingBlockRendered) { await RenderLeadingBlockAsync(); _isLeadingBlockRendered = true; } // If message group is empty or the given message can be grouped, buffer the given message if (!_messageGroupBuffer.Any() || HtmlRenderingLogic.CanBeGrouped(_messageGroupBuffer.Last(), message)) { _messageGroupBuffer.Add(message); } // Otherwise, flush the group and render messages else { await RenderCurrentMessageGroupAsync(); _messageGroupBuffer.Clear(); _messageGroupBuffer.Add(message); } } public override async ValueTask DisposeAsync() { // Leading block (can happen if no message were rendered) if (!_isLeadingBlockRendered) await RenderLeadingBlockAsync(); // Flush current message group if (_messageGroupBuffer.Any()) await RenderCurrentMessageGroupAsync(); // Trailing block await RenderTrailingBlockAsync(); // Dispose stream await base.DisposeAsync(); } } public partial class HtmlMessageRenderer { private static readonly Assembly ResourcesAssembly = typeof(HtmlRenderingLogic).Assembly; private static readonly string ResourcesNamespace = $"{ResourcesAssembly.GetName().Name}.Resources"; private static string GetCoreStyleSheetCode() => ResourcesAssembly .GetManifestResourceString($"{ResourcesNamespace}.HtmlCore.css"); private static string GetThemeStyleSheetCode(string themeName) => ResourcesAssembly .GetManifestResourceString($"{ResourcesNamespace}.Html{themeName}.css"); private static string GetLeadingBlockTemplateCode() => ResourcesAssembly .GetManifestResourceString($"{ResourcesNamespace}.HtmlLayoutTemplate.html") .SubstringUntil("{{~ %SPLIT% ~}}"); private static string GetTrailingBlockTemplateCode() => ResourcesAssembly .GetManifestResourceString($"{ResourcesNamespace}.HtmlLayoutTemplate.html") .SubstringAfter("{{~ %SPLIT% ~}}"); private static string GetMessageGroupTemplateCode() => ResourcesAssembly .GetManifestResourceString($"{ResourcesNamespace}.HtmlMessageGroupTemplate.html"); } }