diff --git a/DiscordChatExporter.Core.Markdown/Nodes/EmojiNode.cs b/DiscordChatExporter.Core.Markdown/Ast/EmojiNode.cs similarity index 91% rename from DiscordChatExporter.Core.Markdown/Nodes/EmojiNode.cs rename to DiscordChatExporter.Core.Markdown/Ast/EmojiNode.cs index 0aa2bd1f..f0d8b0cf 100644 --- a/DiscordChatExporter.Core.Markdown/Nodes/EmojiNode.cs +++ b/DiscordChatExporter.Core.Markdown/Ast/EmojiNode.cs @@ -1,4 +1,4 @@ -namespace DiscordChatExporter.Core.Markdown.Nodes +namespace DiscordChatExporter.Core.Markdown.Ast { public class EmojiNode : Node { diff --git a/DiscordChatExporter.Core.Markdown/Nodes/FormattedNode.cs b/DiscordChatExporter.Core.Markdown/Ast/FormattedNode.cs similarity index 90% rename from DiscordChatExporter.Core.Markdown/Nodes/FormattedNode.cs rename to DiscordChatExporter.Core.Markdown/Ast/FormattedNode.cs index ee540eb6..f4bac0d7 100644 --- a/DiscordChatExporter.Core.Markdown/Nodes/FormattedNode.cs +++ b/DiscordChatExporter.Core.Markdown/Ast/FormattedNode.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace DiscordChatExporter.Core.Markdown.Nodes +namespace DiscordChatExporter.Core.Markdown.Ast { public class FormattedNode : Node { diff --git a/DiscordChatExporter.Core.Markdown/Nodes/InlineCodeBlockNode.cs b/DiscordChatExporter.Core.Markdown/Ast/InlineCodeBlockNode.cs similarity index 82% rename from DiscordChatExporter.Core.Markdown/Nodes/InlineCodeBlockNode.cs rename to DiscordChatExporter.Core.Markdown/Ast/InlineCodeBlockNode.cs index 6cb03917..e68f8a43 100644 --- a/DiscordChatExporter.Core.Markdown/Nodes/InlineCodeBlockNode.cs +++ b/DiscordChatExporter.Core.Markdown/Ast/InlineCodeBlockNode.cs @@ -1,4 +1,4 @@ -namespace DiscordChatExporter.Core.Markdown.Nodes +namespace DiscordChatExporter.Core.Markdown.Ast { public class InlineCodeBlockNode : Node { diff --git a/DiscordChatExporter.Core.Markdown/Nodes/LinkNode.cs b/DiscordChatExporter.Core.Markdown/Ast/LinkNode.cs similarity index 87% rename from DiscordChatExporter.Core.Markdown/Nodes/LinkNode.cs rename to DiscordChatExporter.Core.Markdown/Ast/LinkNode.cs index 5b5b6e6d..c04e9fe2 100644 --- a/DiscordChatExporter.Core.Markdown/Nodes/LinkNode.cs +++ b/DiscordChatExporter.Core.Markdown/Ast/LinkNode.cs @@ -1,4 +1,4 @@ -namespace DiscordChatExporter.Core.Markdown.Nodes +namespace DiscordChatExporter.Core.Markdown.Ast { public class LinkNode : Node { diff --git a/DiscordChatExporter.Core.Markdown/Nodes/MentionNode.cs b/DiscordChatExporter.Core.Markdown/Ast/MentionNode.cs similarity index 85% rename from DiscordChatExporter.Core.Markdown/Nodes/MentionNode.cs rename to DiscordChatExporter.Core.Markdown/Ast/MentionNode.cs index f1b3a794..f532e809 100644 --- a/DiscordChatExporter.Core.Markdown/Nodes/MentionNode.cs +++ b/DiscordChatExporter.Core.Markdown/Ast/MentionNode.cs @@ -1,4 +1,4 @@ -namespace DiscordChatExporter.Core.Markdown.Nodes +namespace DiscordChatExporter.Core.Markdown.Ast { public class MentionNode : Node { diff --git a/DiscordChatExporter.Core.Markdown/Nodes/MentionType.cs b/DiscordChatExporter.Core.Markdown/Ast/MentionType.cs similarity index 64% rename from DiscordChatExporter.Core.Markdown/Nodes/MentionType.cs rename to DiscordChatExporter.Core.Markdown/Ast/MentionType.cs index 6559ebd7..3d7e0934 100644 --- a/DiscordChatExporter.Core.Markdown/Nodes/MentionType.cs +++ b/DiscordChatExporter.Core.Markdown/Ast/MentionType.cs @@ -1,4 +1,4 @@ -namespace DiscordChatExporter.Core.Markdown.Nodes +namespace DiscordChatExporter.Core.Markdown.Ast { public enum MentionType { diff --git a/DiscordChatExporter.Core.Markdown/Nodes/MultiLineCodeBlockNode.cs b/DiscordChatExporter.Core.Markdown/Ast/MultiLineCodeBlockNode.cs similarity index 87% rename from DiscordChatExporter.Core.Markdown/Nodes/MultiLineCodeBlockNode.cs rename to DiscordChatExporter.Core.Markdown/Ast/MultiLineCodeBlockNode.cs index a69f4622..8ecd976f 100644 --- a/DiscordChatExporter.Core.Markdown/Nodes/MultiLineCodeBlockNode.cs +++ b/DiscordChatExporter.Core.Markdown/Ast/MultiLineCodeBlockNode.cs @@ -1,4 +1,4 @@ -namespace DiscordChatExporter.Core.Markdown.Nodes +namespace DiscordChatExporter.Core.Markdown.Ast { public class MultiLineCodeBlockNode : Node { diff --git a/DiscordChatExporter.Core.Markdown/Ast/Node.cs b/DiscordChatExporter.Core.Markdown/Ast/Node.cs new file mode 100644 index 00000000..e591d3c5 --- /dev/null +++ b/DiscordChatExporter.Core.Markdown/Ast/Node.cs @@ -0,0 +1,6 @@ +namespace DiscordChatExporter.Core.Markdown.Ast +{ + public abstract class Node + { + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Core.Markdown/Nodes/TextFormatting.cs b/DiscordChatExporter.Core.Markdown/Ast/TextFormatting.cs similarity index 73% rename from DiscordChatExporter.Core.Markdown/Nodes/TextFormatting.cs rename to DiscordChatExporter.Core.Markdown/Ast/TextFormatting.cs index b9c48130..3e4b49c7 100644 --- a/DiscordChatExporter.Core.Markdown/Nodes/TextFormatting.cs +++ b/DiscordChatExporter.Core.Markdown/Ast/TextFormatting.cs @@ -1,4 +1,4 @@ -namespace DiscordChatExporter.Core.Markdown.Nodes +namespace DiscordChatExporter.Core.Markdown.Ast { public enum TextFormatting { diff --git a/DiscordChatExporter.Core.Markdown/Nodes/TextNode.cs b/DiscordChatExporter.Core.Markdown/Ast/TextNode.cs similarity index 80% rename from DiscordChatExporter.Core.Markdown/Nodes/TextNode.cs rename to DiscordChatExporter.Core.Markdown/Ast/TextNode.cs index a411cee4..6a4a53f0 100644 --- a/DiscordChatExporter.Core.Markdown/Nodes/TextNode.cs +++ b/DiscordChatExporter.Core.Markdown/Ast/TextNode.cs @@ -1,4 +1,4 @@ -namespace DiscordChatExporter.Core.Markdown.Nodes +namespace DiscordChatExporter.Core.Markdown.Ast { public class TextNode : Node { diff --git a/DiscordChatExporter.Core.Markdown/MarkdownParser.cs b/DiscordChatExporter.Core.Markdown/MarkdownParser.cs index 1cac111d..2dfd67d3 100644 --- a/DiscordChatExporter.Core.Markdown/MarkdownParser.cs +++ b/DiscordChatExporter.Core.Markdown/MarkdownParser.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using DiscordChatExporter.Core.Markdown.Ast; using DiscordChatExporter.Core.Markdown.Internal; -using DiscordChatExporter.Core.Markdown.Nodes; namespace DiscordChatExporter.Core.Markdown { diff --git a/DiscordChatExporter.Core.Markdown/Nodes/Node.cs b/DiscordChatExporter.Core.Markdown/Nodes/Node.cs deleted file mode 100644 index 44e3a997..00000000 --- a/DiscordChatExporter.Core.Markdown/Nodes/Node.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace DiscordChatExporter.Core.Markdown.Nodes -{ - public abstract class Node - { - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Core.Rendering/CsvMessageRenderer.cs b/DiscordChatExporter.Core.Rendering/CsvMessageRenderer.cs deleted file mode 100644 index c8370061..00000000 --- a/DiscordChatExporter.Core.Rendering/CsvMessageRenderer.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.IO; -using System.Threading.Tasks; -using DiscordChatExporter.Core.Models; -using DiscordChatExporter.Core.Rendering.Logic; - -namespace DiscordChatExporter.Core.Rendering -{ - public class CsvMessageRenderer : MessageRendererBase - { - private bool _isHeaderRendered; - - public CsvMessageRenderer(TextWriter writer, RenderContext context) - : base(writer, context) - { - } - - public override async Task RenderMessageAsync(Message message) - { - // Render header if it's the first entry - if (!_isHeaderRendered) - { - await Writer.WriteLineAsync(CsvRenderingLogic.FormatHeader(Context)); - _isHeaderRendered = true; - } - - await Writer.WriteLineAsync(CsvRenderingLogic.FormatMessage(Context, message)); - } - } -} diff --git a/DiscordChatExporter.Core.Rendering/FacadeMessageRenderer.cs b/DiscordChatExporter.Core.Rendering/FacadeMessageRenderer.cs deleted file mode 100644 index 22e5033f..00000000 --- a/DiscordChatExporter.Core.Rendering/FacadeMessageRenderer.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using DiscordChatExporter.Core.Models; - -namespace DiscordChatExporter.Core.Rendering -{ - public partial class FacadeMessageRenderer : IMessageRenderer - { - private readonly string _baseFilePath; - private readonly ExportFormat _format; - private readonly RenderContext _context; - private readonly int? _partitionLimit; - - private long _renderedMessageCount; - private int _partitionIndex; - private TextWriter _writer; - private IMessageRenderer _innerRenderer; - - public FacadeMessageRenderer(string baseFilePath, ExportFormat format, RenderContext context, int? partitionLimit) - { - _baseFilePath = baseFilePath; - _format = format; - _context = context; - _partitionLimit = partitionLimit; - } - - private void EnsureInnerRendererInitialized() - { - if (_writer != null && _innerRenderer != null) - return; - - // Get partition file path - var filePath = GetPartitionFilePath(_baseFilePath, _partitionIndex); - - // Create output directory - var dirPath = Path.GetDirectoryName(_baseFilePath); - if (!string.IsNullOrWhiteSpace(dirPath)) - Directory.CreateDirectory(dirPath); - - // Create writer - _writer = File.CreateText(filePath); - - // Create inner renderer - if (_format == ExportFormat.PlainText) - { - _innerRenderer = new PlainTextMessageRenderer(_writer, _context); - } - else if (_format == ExportFormat.Csv) - { - _innerRenderer = new CsvMessageRenderer(_writer, _context); - } - else if (_format == ExportFormat.HtmlDark) - { - _innerRenderer = new HtmlMessageRenderer(_writer, _context, "Dark"); - } - else if (_format == ExportFormat.HtmlLight) - { - _innerRenderer = new HtmlMessageRenderer(_writer, _context, "Light"); - } - else - { - throw new InvalidOperationException($"Unknown export format [{_format}]."); - } - } - - private async Task ResetInnerRendererAsync() - { - if (_innerRenderer != null) - { - await _innerRenderer.DisposeAsync(); - _innerRenderer = null; - } - - if (_writer != null) - { - await _writer.DisposeAsync(); - _writer = null; - } - } - - public async Task RenderMessageAsync(Message message) - { - // Ensure underlying writer and renderer are initialized - EnsureInnerRendererInitialized(); - - // Render the actual message - await _innerRenderer.RenderMessageAsync(message); - - // Increment count - _renderedMessageCount++; - - // Update partition if necessary - if (_partitionLimit != null && _partitionLimit != 0 && _renderedMessageCount % _partitionLimit == 0) - { - await ResetInnerRendererAsync(); - _partitionIndex++; - } - } - - public async ValueTask DisposeAsync() => await ResetInnerRendererAsync(); - } - - public partial class FacadeMessageRenderer - { - private static string GetPartitionFilePath(string baseFilePath, int partitionIndex) - { - // First partition - no changes - if (partitionIndex <= 0) - return baseFilePath; - - // Inject partition index into file name - var fileNameWithoutExt = Path.GetFileNameWithoutExtension(baseFilePath); - var fileExt = Path.GetExtension(baseFilePath); - var fileName = $"{fileNameWithoutExt} [part {partitionIndex + 1}]{fileExt}"; - - // Generate new path - var dirPath = Path.GetDirectoryName(baseFilePath); - if (!string.IsNullOrWhiteSpace(dirPath)) - return Path.Combine(dirPath, fileName); - - return fileName; - } - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Core.Rendering/Formatters/CsvMessageWriter.cs b/DiscordChatExporter.Core.Rendering/Formatters/CsvMessageWriter.cs new file mode 100644 index 00000000..715980ec --- /dev/null +++ b/DiscordChatExporter.Core.Rendering/Formatters/CsvMessageWriter.cs @@ -0,0 +1,25 @@ +using System.IO; +using System.Threading.Tasks; +using DiscordChatExporter.Core.Models; +using DiscordChatExporter.Core.Rendering.Logic; + +namespace DiscordChatExporter.Core.Rendering.Formatters +{ + public class CsvMessageWriter : MessageWriterBase + { + public CsvMessageWriter(TextWriter writer, RenderContext context) + : base(writer, context) + { + } + + public override async Task WritePreambleAsync() + { + await Writer.WriteLineAsync(CsvRenderingLogic.FormatHeader(Context)); + } + + public override async Task WriteMessageAsync(Message message) + { + await Writer.WriteLineAsync(CsvRenderingLogic.FormatMessage(Context, message)); + } + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Core.Rendering/HtmlMessageRenderer.cs b/DiscordChatExporter.Core.Rendering/Formatters/HtmlMessageWriter.cs similarity index 74% rename from DiscordChatExporter.Core.Rendering/HtmlMessageRenderer.cs rename to DiscordChatExporter.Core.Rendering/Formatters/HtmlMessageWriter.cs index c3836c74..54bfc0fe 100644 --- a/DiscordChatExporter.Core.Rendering/HtmlMessageRenderer.cs +++ b/DiscordChatExporter.Core.Rendering/Formatters/HtmlMessageWriter.cs @@ -10,27 +10,25 @@ using Scriban; using Scriban.Runtime; using Tyrrrz.Extensions; -namespace DiscordChatExporter.Core.Rendering +namespace DiscordChatExporter.Core.Rendering.Formatters { - public partial class HtmlMessageRenderer : MessageRendererBase + public partial class HtmlMessageWriter : MessageWriterBase { private readonly string _themeName; private readonly List _messageGroupBuffer = new List(); - private readonly Template _leadingBlockTemplate; + private readonly Template _preambleTemplate; private readonly Template _messageGroupTemplate; - private readonly Template _trailingBlockTemplate; + private readonly Template _postambleTemplate; - private bool _isLeadingBlockRendered; - - public HtmlMessageRenderer(TextWriter writer, RenderContext context, string themeName) + public HtmlMessageWriter(TextWriter writer, RenderContext context, string themeName) : base(writer, context) { _themeName = themeName; - _leadingBlockTemplate = Template.Parse(GetLeadingBlockTemplateCode()); + _preambleTemplate = Template.Parse(GetPreambleTemplateCode()); _messageGroupTemplate = Template.Parse(GetMessageGroupTemplateCode()); - _trailingBlockTemplate = Template.Parse(GetTrailingBlockTemplateCode()); + _postambleTemplate = Template.Parse(GetPostambleTemplateCode()); } private MessageGroup GetCurrentMessageGroup() @@ -82,12 +80,6 @@ namespace DiscordChatExporter.Core.Rendering return templateContext; } - private async Task RenderLeadingBlockAsync() - { - var templateContext = CreateTemplateContext(); - await templateContext.EvaluateAsync(_leadingBlockTemplate.Page); - } - private async Task RenderCurrentMessageGroupAsync() { var templateContext = CreateTemplateContext(new Dictionary @@ -98,21 +90,14 @@ namespace DiscordChatExporter.Core.Rendering await templateContext.EvaluateAsync(_messageGroupTemplate.Page); } - private async Task RenderTrailingBlockAsync() + public override async Task WritePreambleAsync() { var templateContext = CreateTemplateContext(); - await templateContext.EvaluateAsync(_trailingBlockTemplate.Page); + await templateContext.EvaluateAsync(_preambleTemplate.Page); } - public override async Task RenderMessageAsync(Message message) + public override async Task WriteMessageAsync(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)) { @@ -128,25 +113,18 @@ namespace DiscordChatExporter.Core.Rendering } } - public override async ValueTask DisposeAsync() + public override async Task WritePostambleAsync() { - // 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(); + var templateContext = CreateTemplateContext(); + await templateContext.EvaluateAsync(_postambleTemplate.Page); } } - public partial class HtmlMessageRenderer + public partial class HtmlMessageWriter { private static readonly Assembly ResourcesAssembly = typeof(HtmlRenderingLogic).Assembly; private static readonly string ResourcesNamespace = $"{ResourcesAssembly.GetName().Name}.Resources"; @@ -159,18 +137,18 @@ namespace DiscordChatExporter.Core.Rendering ResourcesAssembly .GetManifestResourceString($"{ResourcesNamespace}.Html{themeName}.css"); - private static string GetLeadingBlockTemplateCode() => + private static string GetPreambleTemplateCode() => 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"); + + private static string GetPostambleTemplateCode() => + ResourcesAssembly + .GetManifestResourceString($"{ResourcesNamespace}.HtmlLayoutTemplate.html") + .SubstringAfter("{{~ %SPLIT% ~}}"); } } \ No newline at end of file diff --git a/DiscordChatExporter.Core.Rendering/Formatters/MessageWriterBase.cs b/DiscordChatExporter.Core.Rendering/Formatters/MessageWriterBase.cs new file mode 100644 index 00000000..fc721044 --- /dev/null +++ b/DiscordChatExporter.Core.Rendering/Formatters/MessageWriterBase.cs @@ -0,0 +1,28 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using DiscordChatExporter.Core.Models; + +namespace DiscordChatExporter.Core.Rendering.Formatters +{ + public abstract class MessageWriterBase : IAsyncDisposable + { + protected TextWriter Writer { get; } + + protected RenderContext Context { get; } + + protected MessageWriterBase(TextWriter writer, RenderContext context) + { + Writer = writer; + Context = context; + } + + public virtual Task WritePreambleAsync() => Task.CompletedTask; + + public abstract Task WriteMessageAsync(Message message); + + public virtual Task WritePostambleAsync() => Task.CompletedTask; + + public async ValueTask DisposeAsync() => await Writer.DisposeAsync(); + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Core.Rendering/Formatters/PlainTextMessageWriter.cs b/DiscordChatExporter.Core.Rendering/Formatters/PlainTextMessageWriter.cs new file mode 100644 index 00000000..54768516 --- /dev/null +++ b/DiscordChatExporter.Core.Rendering/Formatters/PlainTextMessageWriter.cs @@ -0,0 +1,26 @@ +using System.IO; +using System.Threading.Tasks; +using DiscordChatExporter.Core.Models; +using DiscordChatExporter.Core.Rendering.Logic; + +namespace DiscordChatExporter.Core.Rendering.Formatters +{ + public class PlainTextMessageWriter : MessageWriterBase + { + public PlainTextMessageWriter(TextWriter writer, RenderContext context) + : base(writer, context) + { + } + + public override async Task WritePreambleAsync() + { + await Writer.WriteLineAsync(PlainTextRenderingLogic.FormatPreamble(Context)); + } + + public override async Task WriteMessageAsync(Message message) + { + await Writer.WriteLineAsync(PlainTextRenderingLogic.FormatMessage(Context, message)); + await Writer.WriteLineAsync(); + } + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Core.Rendering/IMessageRenderer.cs b/DiscordChatExporter.Core.Rendering/IMessageRenderer.cs deleted file mode 100644 index 889c0acf..00000000 --- a/DiscordChatExporter.Core.Rendering/IMessageRenderer.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Threading.Tasks; -using DiscordChatExporter.Core.Models; - -namespace DiscordChatExporter.Core.Rendering -{ - public interface IMessageRenderer : IAsyncDisposable - { - Task RenderMessageAsync(Message message); - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Core.Rendering/Logic/HtmlRenderingLogic.cs b/DiscordChatExporter.Core.Rendering/Logic/HtmlRenderingLogic.cs index b39dca65..cd118bbc 100644 --- a/DiscordChatExporter.Core.Rendering/Logic/HtmlRenderingLogic.cs +++ b/DiscordChatExporter.Core.Rendering/Logic/HtmlRenderingLogic.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Net; using System.Text.RegularExpressions; using DiscordChatExporter.Core.Markdown; -using DiscordChatExporter.Core.Markdown.Nodes; +using DiscordChatExporter.Core.Markdown.Ast; using DiscordChatExporter.Core.Models; using Tyrrrz.Extensions; diff --git a/DiscordChatExporter.Core.Rendering/Logic/PlainTextRenderingLogic.cs b/DiscordChatExporter.Core.Rendering/Logic/PlainTextRenderingLogic.cs index 62da4287..437756e8 100644 --- a/DiscordChatExporter.Core.Rendering/Logic/PlainTextRenderingLogic.cs +++ b/DiscordChatExporter.Core.Rendering/Logic/PlainTextRenderingLogic.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using DiscordChatExporter.Core.Markdown; -using DiscordChatExporter.Core.Markdown.Nodes; +using DiscordChatExporter.Core.Markdown.Ast; using DiscordChatExporter.Core.Models; using DiscordChatExporter.Core.Rendering.Internal; using Tyrrrz.Extensions; diff --git a/DiscordChatExporter.Core.Rendering/MessageRenderer.cs b/DiscordChatExporter.Core.Rendering/MessageRenderer.cs new file mode 100644 index 00000000..c647e08f --- /dev/null +++ b/DiscordChatExporter.Core.Rendering/MessageRenderer.cs @@ -0,0 +1,121 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using DiscordChatExporter.Core.Models; +using DiscordChatExporter.Core.Rendering.Formatters; + +namespace DiscordChatExporter.Core.Rendering +{ + public partial class MessageRenderer : IAsyncDisposable + { + private readonly RenderOptions _options; + private readonly RenderContext _context; + + private long _renderedMessageCount; + private int _partitionIndex; + private MessageWriterBase? _writer; + + public MessageRenderer(RenderOptions options, RenderContext context) + { + _options = options; + _context = context; + } + + private async Task InitializeWriterAsync() + { + // Get partition file path + var filePath = GetPartitionFilePath(_options.BaseFilePath, _partitionIndex); + + // Create output directory + var dirPath = Path.GetDirectoryName(_options.BaseFilePath); + if (!string.IsNullOrWhiteSpace(dirPath)) + Directory.CreateDirectory(dirPath); + + // Create writer + _writer = CreateMessageWriter(filePath, _options.Format, _context); + + // Write preamble + await _writer.WritePreambleAsync(); + } + + private async Task ResetWriterAsync() + { + if (_writer != null) + { + // Write postamble + await _writer.WritePostambleAsync(); + + // Flush + await _writer.DisposeAsync(); + _writer = null; + } + } + + public async Task RenderMessageAsync(Message message) + { + // Ensure underlying writer is initialized + if (_writer == null) + await InitializeWriterAsync(); + + // Render the actual message + await _writer!.WriteMessageAsync(message); + + // Increment count + _renderedMessageCount++; + + // Shift partition if necessary + if (_options.PartitionLimit != null && + _options.PartitionLimit != 0 && + _renderedMessageCount % _options.PartitionLimit == 0) + { + await ResetWriterAsync(); + _partitionIndex++; + } + } + + public async ValueTask DisposeAsync() => await ResetWriterAsync(); + } + + public partial class MessageRenderer + { + private static string GetPartitionFilePath(string baseFilePath, int partitionIndex) + { + // First partition - no changes + if (partitionIndex <= 0) + return baseFilePath; + + // Inject partition index into file name + var fileNameWithoutExt = Path.GetFileNameWithoutExtension(baseFilePath); + var fileExt = Path.GetExtension(baseFilePath); + var fileName = $"{fileNameWithoutExt} [part {partitionIndex + 1}]{fileExt}"; + + // Generate new path + var dirPath = Path.GetDirectoryName(baseFilePath); + if (!string.IsNullOrWhiteSpace(dirPath)) + return Path.Combine(dirPath, fileName); + + return fileName; + } + + private static MessageWriterBase CreateMessageWriter(string filePath, ExportFormat format, RenderContext context) + { + // Create inner writer (it will get disposed by the wrapper) + var writer = File.CreateText(filePath); + + // Create formatter + if (format == ExportFormat.PlainText) + return new PlainTextMessageWriter(writer, context); + + if (format == ExportFormat.Csv) + return new CsvMessageWriter(writer, context); + + if (format == ExportFormat.HtmlDark) + return new HtmlMessageWriter(writer, context, "Dark"); + + if (format == ExportFormat.HtmlLight) + return new HtmlMessageWriter(writer, context, "Light"); + + throw new InvalidOperationException($"Unknown export format [{format}]."); + } + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Core.Rendering/MessageRendererBase.cs b/DiscordChatExporter.Core.Rendering/MessageRendererBase.cs deleted file mode 100644 index 4e9ab9b4..00000000 --- a/DiscordChatExporter.Core.Rendering/MessageRendererBase.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.IO; -using System.Threading.Tasks; -using DiscordChatExporter.Core.Models; - -namespace DiscordChatExporter.Core.Rendering -{ - public abstract class MessageRendererBase : IMessageRenderer - { - protected TextWriter Writer { get; } - - protected RenderContext Context { get; } - - protected MessageRendererBase(TextWriter writer, RenderContext context) - { - Writer = writer; - Context = context; - } - - public abstract Task RenderMessageAsync(Message message); - - public virtual ValueTask DisposeAsync() => default; - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Core.Rendering/PlainTextMessageRenderer.cs b/DiscordChatExporter.Core.Rendering/PlainTextMessageRenderer.cs deleted file mode 100644 index 59fa4f8d..00000000 --- a/DiscordChatExporter.Core.Rendering/PlainTextMessageRenderer.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.IO; -using System.Threading.Tasks; -using DiscordChatExporter.Core.Models; -using DiscordChatExporter.Core.Rendering.Logic; - -namespace DiscordChatExporter.Core.Rendering -{ - public class PlainTextMessageRenderer : MessageRendererBase - { - private bool _isPreambleRendered; - - public PlainTextMessageRenderer(TextWriter writer, RenderContext context) - : base(writer, context) - { - } - - public override async Task RenderMessageAsync(Message message) - { - // Render preamble if it's the first entry - if (!_isPreambleRendered) - { - await Writer.WriteLineAsync(PlainTextRenderingLogic.FormatPreamble(Context)); - _isPreambleRendered = true; - } - - await Writer.WriteLineAsync(PlainTextRenderingLogic.FormatMessage(Context, message)); - await Writer.WriteLineAsync(); - } - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Core.Rendering/RenderOptions.cs b/DiscordChatExporter.Core.Rendering/RenderOptions.cs new file mode 100644 index 00000000..37d09d41 --- /dev/null +++ b/DiscordChatExporter.Core.Rendering/RenderOptions.cs @@ -0,0 +1,20 @@ +using DiscordChatExporter.Core.Models; + +namespace DiscordChatExporter.Core.Rendering +{ + public class RenderOptions + { + public string BaseFilePath { get; } + + public ExportFormat Format { get; } + + public int? PartitionLimit { get; } + + public RenderOptions(string baseFilePath, ExportFormat format, int? partitionLimit) + { + BaseFilePath = baseFilePath; + Format = format; + PartitionLimit = partitionLimit; + } + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Core.Services/ExportService.cs b/DiscordChatExporter.Core.Services/ExportService.cs index f034cb9a..3f1f43b0 100644 --- a/DiscordChatExporter.Core.Services/ExportService.cs +++ b/DiscordChatExporter.Core.Services/ExportService.cs @@ -25,6 +25,12 @@ namespace DiscordChatExporter.Core.Services string outputPath, ExportFormat format, int? partitionLimit, DateTimeOffset? after = null, DateTimeOffset? before = null, IProgress? progress = null) { + // Get base file path from output path + var baseFilePath = GetFilePathFromOutputPath(outputPath, format, guild, channel, after, before); + + // Create options + var options = new RenderOptions(baseFilePath, format, partitionLimit); + // Create context var mentionableUsers = new HashSet(IdBasedEqualityComparer.Instance); var mentionableChannels = await _dataService.GetGuildChannelsAsync(token, guild.Id); @@ -37,8 +43,7 @@ namespace DiscordChatExporter.Core.Services ); // Create renderer - var baseFilePath = GetFilePathFromOutputPath(outputPath, format, context); - await using var renderer = new FacadeMessageRenderer(baseFilePath, format, context, partitionLimit); + await using var renderer = new MessageRenderer(options, context); // Render messages var renderedAnything = false; @@ -61,12 +66,13 @@ namespace DiscordChatExporter.Core.Services public partial class ExportService { - private static string GetFilePathFromOutputPath(string outputPath, ExportFormat format, RenderContext context) + private static string GetFilePathFromOutputPath(string outputPath, ExportFormat format, Guild guild, Channel channel, + DateTimeOffset? after, DateTimeOffset? before) { // Output is a directory if (Directory.Exists(outputPath) || string.IsNullOrWhiteSpace(Path.GetExtension(outputPath))) { - var fileName = ExportLogic.GetDefaultExportFileName(format, context.Guild, context.Channel, context.After, context.Before); + var fileName = ExportLogic.GetDefaultExportFileName(format, guild, channel, after, before); return Path.Combine(outputPath, fileName); }