426 lines
16 KiB
C#
426 lines
16 KiB
C#
|
|
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("&", "&").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($"<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();
|
|||
|
|
}
|