using System.IO.Compression; using System.Text; using System.Text.RegularExpressions; using AuthorBuddy.Web.Models; using DocumentFormat.OpenXml; using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Wordprocessing; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace AuthorBuddy.Web.Services; public partial class ExportService : IExportService { private readonly IProjectService _projectService; public ExportService(IProjectService projectService) { _projectService = projectService; QuestPDF.Settings.License = LicenseType.Community; } public async Task ExportAsync(int projectId, List filePaths, ExportFormat format) { var project = await _projectService.GetProjectAsync(projectId); var projectName = project?.Name ?? "Projekt"; var files = new List<(string name, string content)>(); foreach (var path in filePaths) { if (File.Exists(path)) { var content = await File.ReadAllTextAsync(path, Encoding.UTF8); var name = Path.GetFileName(path); files.Add((name, content)); } } return format switch { ExportFormat.Html => GenerateHtml(projectName, files), ExportFormat.Markdown => GenerateMarkdown(projectName, files), ExportFormat.Zip => GenerateZip(files), ExportFormat.Docx => GenerateDocx(projectName, files), ExportFormat.Pdf => GeneratePdf(projectName, files), _ => throw new ArgumentOutOfRangeException(nameof(format)) }; } private static byte[] GenerateHtml(string projectName, List<(string name, string content)> files) { var sb = new StringBuilder(); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine($"{EscapeHtml(projectName)} – Export"); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine($"

{EscapeHtml(projectName)}

Exportiert: {DateTime.Now:dd.MM.yyyy HH:mm}

"); foreach (var (name, content) in files) { sb.AppendLine($"

{EscapeHtml(name)}

{RenderMarkdownAsHtml(content)}
"); } sb.AppendLine(""); sb.AppendLine(""); return Encoding.UTF8.GetBytes(sb.ToString()); } private static byte[] GenerateMarkdown(string projectName, List<(string name, string content)> files) { var sb = new StringBuilder(); sb.AppendLine($"# {projectName}"); sb.AppendLine(); sb.AppendLine($"> Exportiert: {DateTime.Now:dd.MM.yyyy HH:mm}"); sb.AppendLine(); sb.AppendLine("---"); sb.AppendLine(); foreach (var (name, content) in files) { sb.AppendLine($"## {name}"); sb.AppendLine(); sb.AppendLine(content.TrimEnd()); sb.AppendLine(); sb.AppendLine("---"); sb.AppendLine(); } return Encoding.UTF8.GetBytes(sb.ToString()); } private static byte[] GenerateZip(List<(string name, string content)> files) { using var ms = new MemoryStream(); using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, true)) { foreach (var (name, content) in files) { var entry = archive.CreateEntry(name, CompressionLevel.Optimal); using var writer = new StreamWriter(entry.Open(), Encoding.UTF8); writer.Write(content); } } return ms.ToArray(); } private static byte[] GenerateDocx(string projectName, List<(string name, string content)> files) { using var ms = new MemoryStream(); using var doc = WordprocessingDocument.Create(ms, WordprocessingDocumentType.Document); var mainPart = doc.AddMainDocumentPart(); mainPart.Document = new DocumentFormat.OpenXml.Wordprocessing.Document(); var body = mainPart.Document.AppendChild(new Body()); body.AppendChild(new Paragraph( new Run(new RunProperties(new Bold()), new Text(projectName))) { ParagraphProperties = new ParagraphProperties(new Justification { Val = JustificationValues.Center }) }); body.AppendChild(new Paragraph( new Run(new RunProperties(new DocumentFormat.OpenXml.Wordprocessing.Color { Val = "666666" }, new FontSize { Val = "20" }), new Text($"Exportiert: {DateTime.Now:dd.MM.yyyy HH:mm}")))); foreach (var (name, content) in files) { body.AppendChild(new Paragraph( new Run(new RunProperties(new Bold(), new FontSize { Val = "32" }), new Text(name)) )); var lines = content.Split('\n'); foreach (var raw in lines) { var line = raw.TrimEnd('\r'); if (string.IsNullOrWhiteSpace(line)) { body.AppendChild(new Paragraph()); continue; } if (line.StartsWith("### ")) { body.AppendChild(new Paragraph( new Run(new RunProperties(new Bold(), new FontSize { Val = "28" }), new Text(line[4..])) )); } else if (line.StartsWith("## ")) { body.AppendChild(new Paragraph( new Run(new RunProperties(new Bold(), new FontSize { Val = "32" }), new Text(line[3..])) )); } else if (line.StartsWith("# ")) { body.AppendChild(new Paragraph( new Run(new RunProperties(new Bold(), new FontSize { Val = "36" }), new Text(line[2..])) )); } else if (line.StartsWith("- ") || line.StartsWith("* ")) { body.AppendChild(new Paragraph( new Run(new Text($" • {line[2..]}")) )); } else if (line.Length > 2 && char.IsDigit(line[0]) && line[1] == '.' && line[2] == ' ') { body.AppendChild(new Paragraph( new Run(new Text($" {line}")) )); } else { body.AppendChild(new Paragraph( new Run(new Text(line)) )); } } } doc.Save(); return ms.ToArray(); } private static byte[] GeneratePdf(string projectName, List<(string name, string content)> files) { var doc = QuestPDF.Fluent.Document.Create(container => { container.Page(page => { page.Size(PageSizes.A4); page.Margin(40); page.DefaultTextStyle(x => x.FontSize(11).FontFamily("Times New Roman")); page.Header().Column(col => { col.Item().AlignCenter().Text(projectName).Bold().FontSize(22); col.Item().AlignCenter().Text($"Exportiert: {DateTime.Now:dd.MM.yyyy HH:mm}").FontSize(10).FontColor(Colors.Grey.Medium); }); page.Content().Column(col => { foreach (var (name, content) in files) { col.Item().PaddingTop(12).Text(name).Bold().FontSize(16); col.Item().PaddingLeft(4).Element(c => RenderMarkdownToPdf(c, content)); } }); page.Footer().AlignCenter().Text(x => { x.Span("Seite ").FontSize(9); x.CurrentPageNumber().FontSize(9); }); }); }); using var ms = new MemoryStream(); doc.GeneratePdf(ms); return ms.ToArray(); } private static void RenderMarkdownToPdf(IContainer container, string md) { var lines = md.Split('\n'); container.Column(col => { foreach (var raw in lines) { var line = raw.TrimEnd('\r'); if (string.IsNullOrWhiteSpace(line)) { col.Item().Height(8); continue; } if (line.StartsWith("### ")) { col.Item().PaddingTop(6).Text(line[4..]).Bold().FontSize(13); } else if (line.StartsWith("## ")) { col.Item().PaddingTop(8).Text(line[3..]).Bold().FontSize(15); } else if (line.StartsWith("# ")) { col.Item().PaddingTop(10).Text(line[2..]).Bold().FontSize(17); } else if (line.StartsWith("- ") || line.StartsWith("* ")) { col.Item().PaddingLeft(12).Text($" • {line[2..]}").FontSize(11); } else if (line is "---" or "***" or "___") { col.Item().Height(1).Background(Colors.Grey.Lighten2); } else if (line.Length > 2 && char.IsDigit(line[0]) && line[1] == '.' && line[2] == ' ') { col.Item().PaddingLeft(12).Text($" {line}").FontSize(11); } else { col.Item().Text(line).FontSize(11); } } }); } private static string EscapeHtml(string text) => text.Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("\"", """); private static string RenderMarkdownAsHtml(string md) { var lines = md.Split('\n'); var sb = new StringBuilder(); var inList = false; foreach (var raw in lines) { var line = raw.TrimEnd('\r'); if (line.StartsWith("###### ")) { CloseList(ref inList, sb); sb.AppendLine($"
{EscapeHtml(line[7..])}
"); continue; } if (line.StartsWith("##### ")) { CloseList(ref inList, sb); sb.AppendLine($"
{EscapeHtml(line[6..])}
"); continue; } if (line.StartsWith("#### ")) { CloseList(ref inList, sb); sb.AppendLine($"

{EscapeHtml(line[5..])}

"); continue; } if (line.StartsWith("### ")) { CloseList(ref inList, sb); sb.AppendLine($"

{EscapeHtml(line[4..])}

"); continue; } if (line.StartsWith("## ")) { CloseList(ref inList, sb); sb.AppendLine($"

{EscapeHtml(line[3..])}

"); continue; } if (line.StartsWith("# ")) { CloseList(ref inList, sb); sb.AppendLine($"

{EscapeHtml(line[2..])}

"); continue; } if (line.StartsWith("- ") || line.StartsWith("* ")) { if (!inList) { inList = true; sb.AppendLine(""); inList = false; } } private static string RenderInline(string text) { var result = EscapeHtml(text); result = BoldRegex().Replace(result, "$1"); result = ItalicRegex().Replace(result, "$1"); result = UnderlineRegex().Replace(result, "$1"); result = CodeRegex().Replace(result, "$1"); result = LinkRegex().Replace(result, "$1"); return result; } private static string GetHtmlCss() { return """ * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: Georgia, 'Times New Roman', serif; font-size: 12pt; line-height: 1.6; color: #222; max-width: 210mm; margin: 0 auto; padding: 20mm 30mm; background: #fff; } .doc-header { text-align: center; margin-bottom: 30px; padding-bottom: 15px; border-bottom: 2px solid #333; } .doc-header h1 { font-size: 22pt; margin-bottom: 4px; } .doc-date { font-size: 10pt; color: #666; } .doc-section { margin-bottom: 25px; page-break-before: auto; } .doc-section h2 { font-size: 16pt; margin-bottom: 10px; padding-bottom: 3px; border-bottom: 1px solid #ccc; color: #333; } .doc-content h1 { font-size: 18pt; margin: 15px 0 8px; } .doc-content h2 { font-size: 15pt; margin: 12px 0 6px; } .doc-content h3 { font-size: 13pt; margin: 10px 0 5px; } .doc-content h4 { font-size: 12pt; margin: 8px 0 4px; } .doc-content h5 { font-size: 11pt; margin: 6px 0 3px; } .doc-content h6 { font-size: 10pt; margin: 6px 0 3px; color: #555; } .doc-content p { margin: 6px 0; text-align: justify; } .doc-content ul, .doc-content ol { margin: 6px 0 6px 20px; } .doc-content li { margin: 2px 0; } .doc-content hr { margin: 15px 0; border: none; border-top: 1px solid #ccc; } .doc-content code { font-family: 'Consolas', 'Courier New', monospace; font-size: 10pt; background: #f4f4f4; padding: 1px 4px; border-radius: 2px; } .doc-content a { color: #1a73e8; text-decoration: underline; } .doc-content strong { font-weight: bold; } .doc-content em { font-style: italic; } @media print { body { padding: 0; } .doc-section { page-break-before: always; } .doc-section:first-of-type { page-break-before: auto; } } """; } [GeneratedRegex(@"\*\*(.+?)\*\*")] private static partial Regex BoldRegex(); [GeneratedRegex(@"\*(.+?)\*")] private static partial Regex ItalicRegex(); [GeneratedRegex(@"(.+?)")] private static partial Regex UnderlineRegex(); [GeneratedRegex(@"`(.+?)`")] private static partial Regex CodeRegex(); [GeneratedRegex(@"\[([^\]]+)\]\(([^)]+)\)")] private static partial Regex LinkRegex(); }