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.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 0e884234-17af-436a-a1a7-65cdf2c77dfe
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/EWwamzQ
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
riyadhafraa
2026-05-04 14:51:45 +00:00
parent 74d6fb0969
commit da07d9665a
@@ -118,6 +118,25 @@ function htmlToPlain(input: string | null | undefined): string {
.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(
startTime: string | null,
endTime: string | null,
@@ -199,7 +218,7 @@ function buildAttendeesHtml(attendees: PdfMeetingAttendee[]): string {
function buildMeetingRow(meeting: PdfMeeting, input: RenderPdfInput): string {
const isRtl = input.lang === "ar";
const title = htmlToPlain(
const titleHtml = htmlToSafeHtml(
isRtl ? meeting.titleAr : meeting.titleEn || meeting.titleAr,
);
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[] = [
{ 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: `<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];
if (startIdx !== undefined && endIdx !== undefined && startIdx <= endIdx) {
const colspan = endIdx - startIdx + 1;
const mergeText = meeting.mergeText ? htmlToPlain(meeting.mergeText) : "";
const mergeHtml = meeting.mergeText ? htmlToSafeHtml(meeting.mergeText) : "";
const mergedCell: TableCell = {
html: esc(mergeText),
html: mergeHtml,
cls: "merged-cell",
style: "",
colspan,
@@ -272,7 +291,7 @@ export function buildScheduleHtml(input: RenderPdfInput): string {
const colWidths = ["8%", "30%", "42%", "20%"];
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
@@ -323,27 +342,28 @@ body {
print-color-adjust: exact;
}
.page-header {
text-align: center;
margin-bottom: 10px;
position: relative;
min-height: ${logoHtml ? "55px" : "auto"};
.pdf-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
direction: rtl;
}
.page-header .logo {
position: absolute;
${isRtl ? "right" : "left"}: 0;
top: 0;
}
.page-header h1 {
.pdf-title {
color: #002060;
font-size: ${Math.min(22, fontSize + 6)}px;
font-weight: bold;
color: ${fontColor};
text-align: right;
margin: 0;
padding: 10px 0;
}
.pdf-logo img {
max-height: 50px;
object-fit: contain;
}
table {
width: 100%;
border-collapse: collapse;
@@ -447,9 +467,9 @@ tr {
</head>
<body>
<div class="page-header">
${logoHtml ? `<div class="logo">${logoHtml}</div>` : ""}
<h1>${esc(input.labels.title)}</h1>
<div class="pdf-header">
<h1 class="pdf-title">${esc(input.labels.title)}</h1>
${logoHtml ? `<div class="pdf-logo">${logoHtml}</div>` : ""}
</div>
<table>