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(""); list = false; } if (bullet) body.Append("
  • ").Append(ThemedCvRenderer.Inline(line[2..])).Append("
  • "); else if (line.Length > 0) body.Append("

    ").Append(ThemedCvRenderer.Inline(line)).Append("

    "); else body.Append("
    "); } if (list) body.Append(""); 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) ? "" : $"

    {E(s.SenderName)}

    "; var subjectHeading = string.IsNullOrWhiteSpace(s.Subject) ? "" : $"

    {E(s.Subject)}

    "; var html = $$""" {{E(string.IsNullOrEmpty(title) ? letterLabel : title)}}
    {{senderHeading}}
    {{E(s.SenderTitle)}}
    {{contact}}
    {{E(s.Date)}}
    {{E(s.Recipient)}}
    {{subjectHeading}}
    {{body}}
    """; 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("", css + "", 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"); } }