Improve PDF generation by preserving text formatting and updating header layout

Introduce `htmlToSafeHtml` function to retain text formatting (color, bold) in PDFs and restructure the header layout, moving the logo to the right and title to the left.
This commit is contained in:
Riyadh
2026-05-04 14:51:45 +00:00
parent f7c29426fa
commit ca5d4aa860
@@ -118,6 +118,25 @@ function htmlToPlain(input: string | null | undefined): string {
.trim(); .trim();
} }
function htmlToSafeHtml(input: string | null | undefined): string {
if (!input) return "";
const withBreaks = input
.replace(/<\/(p|div)>/gi, "<br/>")
.replace(/<(p|div)(\s[^>]*)?>/gi, "");
const sanitized = sanitizeHtml(withBreaks, {
allowedTags: ["span", "br", "strong", "b", "em", "i"],
allowedAttributes: { span: ["style"] },
allowedStyles: {
span: {
color: [/.*/],
"font-weight": [/.*/],
"background-color": [/.*/],
},
},
});
return sanitized.replace(/(<br\s*\/?>)+$/gi, "").trim();
}
function formatTimeRange( function formatTimeRange(
startTime: string | null, startTime: string | null,
endTime: string | null, endTime: string | null,
@@ -199,7 +218,7 @@ function buildAttendeesHtml(attendees: PdfMeetingAttendee[]): string {
function buildMeetingRow(meeting: PdfMeeting, input: RenderPdfInput): string { function buildMeetingRow(meeting: PdfMeeting, input: RenderPdfInput): string {
const isRtl = input.lang === "ar"; const isRtl = input.lang === "ar";
const title = htmlToPlain( const titleHtml = htmlToSafeHtml(
isRtl ? meeting.titleAr : meeting.titleEn || meeting.titleAr, isRtl ? meeting.titleAr : meeting.titleEn || meeting.titleAr,
); );
const location = meeting.location ? `<br/><span style="font-size:90%">${esc(meeting.location)}</span>` : ""; const location = meeting.location ? `<br/><span style="font-size:90%">${esc(meeting.location)}</span>` : "";
@@ -221,7 +240,7 @@ function buildMeetingRow(meeting: PdfMeeting, input: RenderPdfInput): string {
const cellsLtr: TableCell[] = [ const cellsLtr: TableCell[] = [
{ html: esc(String(meeting.dailyNumber)), cls: "num-cell", style: numDarkStyle }, { html: esc(String(meeting.dailyNumber)), cls: "num-cell", style: numDarkStyle },
{ html: esc(title) + location, cls: "meeting-cell", style: coloredStyle }, { html: titleHtml + location, cls: "meeting-cell", style: coloredStyle },
{ html: attendeesHtml, cls: "attendees-cell", style: "" }, { html: attendeesHtml, cls: "attendees-cell", style: "" },
{ html: `<span dir="ltr">${esc(time)}</span>`, cls: "time-cell", style: "" }, { html: `<span dir="ltr">${esc(time)}</span>`, cls: "time-cell", style: "" },
]; ];
@@ -240,9 +259,9 @@ function buildMeetingRow(meeting: PdfMeeting, input: RenderPdfInput): string {
const endIdx = MERGE_COL_INDEX[meeting.mergeEndColumn]; const endIdx = MERGE_COL_INDEX[meeting.mergeEndColumn];
if (startIdx !== undefined && endIdx !== undefined && startIdx <= endIdx) { if (startIdx !== undefined && endIdx !== undefined && startIdx <= endIdx) {
const colspan = endIdx - startIdx + 1; const colspan = endIdx - startIdx + 1;
const mergeText = meeting.mergeText ? htmlToPlain(meeting.mergeText) : ""; const mergeHtml = meeting.mergeText ? htmlToSafeHtml(meeting.mergeText) : "";
const mergedCell: TableCell = { const mergedCell: TableCell = {
html: esc(mergeText), html: mergeHtml,
cls: "merged-cell", cls: "merged-cell",
style: "", style: "",
colspan, colspan,
@@ -272,7 +291,7 @@ export function buildScheduleHtml(input: RenderPdfInput): string {
const colWidths = ["8%", "30%", "42%", "20%"]; const colWidths = ["8%", "30%", "42%", "20%"];
const logoHtml = input.logo && input.logo.length > 0 const logoHtml = input.logo && input.logo.length > 0
? `<img src="data:image/png;base64,${input.logo.toString("base64")}" style="height:50px;width:auto;" />` ? `<img src="data:image/png;base64,${input.logo.toString("base64")}" />`
: ""; : "";
const headerCells = headerLabels const headerCells = headerLabels
@@ -323,27 +342,28 @@ body {
print-color-adjust: exact; print-color-adjust: exact;
} }
.page-header { .pdf-header {
text-align: center; display: flex;
margin-bottom: 10px; justify-content: space-between;
position: relative; align-items: center;
min-height: ${logoHtml ? "55px" : "auto"}; margin-bottom: 16px;
direction: rtl;
} }
.page-header .logo { .pdf-title {
position: absolute; color: #002060;
${isRtl ? "right" : "left"}: 0;
top: 0;
}
.page-header h1 {
font-size: ${Math.min(22, fontSize + 6)}px; font-size: ${Math.min(22, fontSize + 6)}px;
font-weight: bold; font-weight: bold;
color: ${fontColor}; text-align: right;
margin: 0; margin: 0;
padding: 10px 0; padding: 10px 0;
} }
.pdf-logo img {
max-height: 50px;
object-fit: contain;
}
table { table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
@@ -447,9 +467,9 @@ tr {
</head> </head>
<body> <body>
<div class="page-header"> <div class="pdf-header">
${logoHtml ? `<div class="logo">${logoHtml}</div>` : ""} <h1 class="pdf-title">${esc(input.labels.title)}</h1>
<h1>${esc(input.labels.title)}</h1> ${logoHtml ? `<div class="pdf-logo">${logoHtml}</div>` : ""}
</div> </div>
<table> <table>