AuthorBuddy/Services/ExportService.cs
2026-05-31 19:31:27 +02:00

425 lines
16 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<byte[]> ExportAsync(int projectId, List<string> 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("<!DOCTYPE html>");
sb.AppendLine("<html lang=\"de\">");
sb.AppendLine("<head>");
sb.AppendLine("<meta charset=\"utf-8\">");
sb.AppendLine("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
sb.AppendLine($"<title>{EscapeHtml(projectName)} Export</title>");
sb.AppendLine("<style>");
sb.AppendLine(GetHtmlCss());
sb.AppendLine("</style>");
sb.AppendLine("</head>");
sb.AppendLine("<body>");
sb.AppendLine($"<header class=\"doc-header\"><h1>{EscapeHtml(projectName)}</h1><p class=\"doc-date\">Exportiert: {DateTime.Now:dd.MM.yyyy HH:mm}</p></header>");
foreach (var (name, content) in files)
{
sb.AppendLine($"<section class=\"doc-section\"><h2>{EscapeHtml(name)}</h2><div class=\"doc-content\">{RenderMarkdownAsHtml(content)}</div></section>");
}
sb.AppendLine("</body>");
sb.AppendLine("</html>");
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("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;").Replace("\"", "&quot;");
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($"<h6>{EscapeHtml(line[7..])}</h6>");
continue;
}
if (line.StartsWith("##### "))
{
CloseList(ref inList, sb);
sb.AppendLine($"<h5>{EscapeHtml(line[6..])}</h5>");
continue;
}
if (line.StartsWith("#### "))
{
CloseList(ref inList, sb);
sb.AppendLine($"<h4>{EscapeHtml(line[5..])}</h4>");
continue;
}
if (line.StartsWith("### "))
{
CloseList(ref inList, sb);
sb.AppendLine($"<h3>{EscapeHtml(line[4..])}</h3>");
continue;
}
if (line.StartsWith("## "))
{
CloseList(ref inList, sb);
sb.AppendLine($"<h2>{EscapeHtml(line[3..])}</h2>");
continue;
}
if (line.StartsWith("# "))
{
CloseList(ref inList, sb);
sb.AppendLine($"<h1>{EscapeHtml(line[2..])}</h1>");
continue;
}
if (line.StartsWith("- ") || line.StartsWith("* "))
{
if (!inList)
{
inList = true;
sb.AppendLine("<ul>");
}
sb.AppendLine($"<li>{RenderInline(line[2..])}</li>");
continue;
}
if (line.Length > 2 && char.IsDigit(line[0]) && (line[1] == '.' || line[1] == ')') && line[2] == ' ')
{
if (!inList)
{
inList = true;
sb.AppendLine("<ol>");
}
sb.AppendLine($"<li>{RenderInline(line[3..])}</li>");
continue;
}
if (line is "---" or "***" or "___")
{
CloseList(ref inList, sb);
sb.AppendLine("<hr>");
continue;
}
if (string.IsNullOrWhiteSpace(line))
{
CloseList(ref inList, sb);
continue;
}
CloseList(ref inList, sb);
sb.AppendLine($"<p>{RenderInline(line)}</p>");
}
CloseList(ref inList, sb);
return sb.ToString();
}
private static void CloseList(ref bool inList, StringBuilder sb)
{
if (inList)
{
sb.AppendLine(sb[sb.Length - 1] == '>' ? "</ol>" : "</ul>");
inList = false;
}
}
private static string RenderInline(string text)
{
var result = EscapeHtml(text);
result = BoldRegex().Replace(result, "<strong>$1</strong>");
result = ItalicRegex().Replace(result, "<em>$1</em>");
result = UnderlineRegex().Replace(result, "<u>$1</u>");
result = CodeRegex().Replace(result, "<code>$1</code>");
result = LinkRegex().Replace(result, "<a href=\"$2\">$1</a>");
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(@"<u>(.+?)</u>")]
private static partial Regex UnderlineRegex();
[GeneratedRegex(@"`(.+?)`")]
private static partial Regex CodeRegex();
[GeneratedRegex(@"\[([^\]]+)\]\(([^)]+)\)")]
private static partial Regex LinkRegex();
}