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:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user