93 lines
7.2 KiB
C#
93 lines
7.2 KiB
C#
using System.Globalization;
|
|
using System.Net;
|
|
using System.Text;
|
|
using JobTrackerApi.Models;
|
|
|
|
namespace JobTrackerApi.Services;
|
|
|
|
public sealed record CoverLetterRenderDto(string Html, string ThemeId, string SuggestedFileName);
|
|
|
|
public sealed class CoverLetterDocumentRenderer
|
|
{
|
|
public CoverLetterRenderDto Render(string? text, CoverLetterDocumentSettings? settings)
|
|
{
|
|
var s = CoverLetterDocumentSettingsJson.Normalize(settings);
|
|
static string E(string? value) => WebUtility.HtmlEncode(value ?? "");
|
|
static string F(double? value) => value!.Value.ToString("0.##", CultureInfo.InvariantCulture);
|
|
var font = s.BodyFont!;
|
|
var theme = CvThemeCatalog.Resolve(s.ThemeId, s.DesignVersion);
|
|
var fonts = s.DesignVersion == 2 ? CvDocumentFonts.Embed(font, s.HeadingFont!, theme.UtilityFont) : CvDocumentFonts.Embed(font);
|
|
var size = s.PageSize == "letter" ? "215.9mm 279.4mm" : "210mm 297mm";
|
|
var width = s.PageSize == "letter" ? "215.9mm" : "210mm";
|
|
var height = s.PageSize == "letter" ? "279.4mm" : "297mm";
|
|
var body = new StringBuilder();
|
|
var list = false;
|
|
foreach (var line in (text ?? "").Replace("\r\n", "\n").Replace('\r', '\n').Split('\n'))
|
|
{
|
|
var bullet = line.StartsWith("- ", StringComparison.Ordinal) || line.StartsWith("* ", StringComparison.Ordinal);
|
|
if (bullet && !list) { body.Append("<ul>"); list = true; }
|
|
if (!bullet && list) { body.Append("</ul>"); list = false; }
|
|
if (bullet) body.Append("<li>").Append(ThemedCvRenderer.Inline(line[2..])).Append("</li>");
|
|
else if (line.Length > 0) body.Append("<p>").Append(ThemedCvRenderer.Inline(line)).Append("</p>");
|
|
else body.Append("<div class=\"paragraph-gap\"></div>");
|
|
}
|
|
if (list) body.Append("</ul>");
|
|
var contact = string.Join(" · ", new[] { s.SenderEmail, s.SenderPhone, s.SenderLocation }.Where(v => !string.IsNullOrWhiteSpace(v)).Select(E));
|
|
var letterLabel = s.Language == "nb-NO" ? "Søknadsbrev" : "Cover letter";
|
|
var bodyLabel = s.Language == "nb-NO" ? "Brevtekst" : "Cover letter text";
|
|
var title = string.Join(" — ", new[] { s.Subject, s.SenderName }.Where(value => !string.IsNullOrWhiteSpace(value)));
|
|
var senderHeading = string.IsNullOrWhiteSpace(s.SenderName) ? "" : $"<h1>{E(s.SenderName)}</h1>";
|
|
var subjectHeading = string.IsNullOrWhiteSpace(s.Subject) ? "" : $"<h2>{E(s.Subject)}</h2>";
|
|
var html = $$"""
|
|
<!DOCTYPE html><html lang="{{E(s.Language)}}"><head><meta charset="utf-8">
|
|
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; font-src data:; base-uri 'none'">
|
|
<title>{{E(string.IsNullOrEmpty(title) ? letterLabel : title)}}</title><style>
|
|
{{fonts}}
|
|
:root{--cv-page-margin:{{F(s.PageMarginMm)}}mm}
|
|
*{box-sizing:border-box}html,body{margin:0;padding:0;color:#111827;background:#fff}
|
|
body{font-family:{{CvDocumentFonts.Stack(font)}};font-size:{{F(s.BaseFontSizePt)}}pt;line-height:{{F(s.LineHeight)}}}
|
|
.letter-paper{width:{{width}};min-height:{{height}};padding:{{F(s.PageMarginMm)}}mm;margin:0 auto;overflow-wrap:anywhere}
|
|
header{border-bottom:1mm solid {{s.AccentColor}};padding-bottom:5mm;margin-bottom:7mm;break-inside:avoid}
|
|
h1{font-size:20pt;line-height:1.2;margin:0 0 2mm;color:{{s.AccentColor}}}h2{font-size:12pt;margin:5mm 0;break-after:avoid}
|
|
.contact{margin-top:2mm}.recipient{white-space:pre-wrap;margin:5mm 0}.date{text-align:right}p{margin:0 0 2mm;orphans:3;widows:3}
|
|
.paragraph-gap{height:2mm}ul{padding-left:6mm;margin:2mm 0}li{orphans:3;widows:3}a{color:inherit;text-decoration:underline}
|
|
@page{size:{{size}};margin:{{F(s.PageMarginMm)}}mm 0}
|
|
@media print{.letter-paper{width:auto;min-height:0;padding:0 {{F(s.PageMarginMm)}}mm;margin:0} }
|
|
</style></head><body><article class="page letter-paper" aria-label="{{E(letterLabel)}}">
|
|
<header>{{senderHeading}}<div>{{E(s.SenderTitle)}}</div><div class="contact">{{contact}}</div></header>
|
|
<div class="date">{{E(s.Date)}}</div><div class="recipient">{{E(s.Recipient)}}</div>{{subjectHeading}}
|
|
<main aria-label="{{E(bodyLabel)}}">{{body}}</main></article></body></html>
|
|
""";
|
|
if (s.DesignVersion == 2)
|
|
{
|
|
var css = $"h1,h2{{font-family:{CvDocumentFonts.Stack(s.HeadingFont!)};}}h1{{font-size:{F(theme.NameSizePt - 2)}pt;}}header{{border-bottom-width:.4mm;}}";
|
|
css += theme.HeaderStyle switch
|
|
{
|
|
"band" when theme.Layout == "header-band" => $"header{{background:{s.AccentColor};color:{ThemedCvRenderer.ContrastInk(s.AccentColor!)};padding:6mm;margin-bottom:8mm;border:0;}}header h1{{color:inherit;}}",
|
|
"centered" => "header{text-align:center;padding-bottom:7mm;margin-bottom:9mm;}h1{font-weight:600;}",
|
|
"kicker" => $"header::before{{content:'{letterLabel}';display:block;letter-spacing:.2em;text-transform:uppercase;color:{s.AccentColor};font-size:8pt;margin-bottom:3mm;}}",
|
|
_ => "header{padding-bottom:6mm;margin-bottom:8mm;}",
|
|
};
|
|
if (theme.HeadingStyle == "plain" && theme.HeaderStyle == "plain") css += "header{border:0;}";
|
|
if (theme.NumberSections) css += $".contact,.date{{font-family:{CvDocumentFonts.Stack(theme.UtilityFont)};font-size:9pt;}}";
|
|
if (theme.Layout is "sidebar-left" or "sidebar-right")
|
|
{
|
|
var right = theme.Layout == "sidebar-right";
|
|
var edge = right ? "right" : "left";
|
|
var headerColumn = right ? 2 : 1;
|
|
var contentColumn = right ? 1 : 2;
|
|
var columns = right ? $"minmax(0,1fr) {F(theme.SidebarWidthMm)}mm" : $"{F(theme.SidebarWidthMm)}mm minmax(0,1fr)";
|
|
css += $".letter-paper{{display:grid;grid-template-columns:{columns};grid-template-rows:auto auto auto auto;align-content:start;padding:0;background:transparent;position:relative;z-index:1;}}" +
|
|
$".letter-paper>header{{grid-column:{headerColumn};grid-row:1 / span 4;padding:8mm;color:{theme.SidebarInk};border:0;margin:0;}}header h1{{color:inherit;}}" +
|
|
$".letter-paper>.date,.letter-paper>.recipient,.letter-paper>h2,.letter-paper>main{{grid-column:{contentColumn};padding-left:{F(s.PageMarginMm)}mm;padding-right:{F(s.PageMarginMm)}mm;background:white;min-width:0;}}" +
|
|
$"body::before{{content:'';position:fixed;{edge}:0;top:0;bottom:0;width:{F(theme.SidebarWidthMm)}mm;background:{theme.SidebarBg};}}" +
|
|
$"@page{{@top-{edge}{{content:'';background:{theme.SidebarBg};width:{F(theme.SidebarWidthMm)}mm;}}@bottom-{edge}{{content:'';background:{theme.SidebarBg};width:{F(theme.SidebarWidthMm)}mm;}}}}" +
|
|
"@media print{.letter-paper{padding:0;}}";
|
|
}
|
|
html = html.Replace("</style>", css + "</style>", StringComparison.Ordinal)
|
|
.Replace("class=\"page letter-paper\"", $"class=\"page letter-paper letter-theme-{theme.Id}\"", StringComparison.Ordinal);
|
|
}
|
|
return new(html, s.ThemeId, "cover-letter.pdf");
|
|
}
|
|
}
|