/*
* Copyright (C) 2026 Mateusz Gruszczynski @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
import { t } from "@rustpad/i18n";
import { toast } from "@rustpad/toast";
import { loadHighlight } from "@rustpad/vendor-libs";
const STORAGE_PREFIX = "rustpad:pdf-export:";
const PAGE_FORMATS = Object.freeze({
"a4-portrait": { cssSize: "A4 portrait", widthMm: 210, heightMm: 297 },
"a4-landscape": { cssSize: "A4 landscape", widthMm: 297, heightMm: 210 },
"a3-portrait": { cssSize: "A3 portrait", widthMm: 297, heightMm: 420 },
"a3-landscape": { cssSize: "A3 landscape", widthMm: 420, heightMm: 297 },
"a5-portrait": { cssSize: "A5 portrait", widthMm: 148, heightMm: 210 },
"letter-portrait": { cssSize: "letter portrait", widthMm: 215.9, heightMm: 279.4 },
"letter-landscape": { cssSize: "letter landscape", widthMm: 279.4, heightMm: 215.9 },
"screen-16-9": { cssSize: "338.667mm 190.5mm", widthMm: 338.667, heightMm: 190.5 },
"screen-4-3": { cssSize: "254mm 190.5mm", widthMm: 254, heightMm: 190.5 },
});
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function saved(key, fallback) {
try { return localStorage.getItem(`${STORAGE_PREFIX}${key}`) || fallback; } catch { return fallback; }
}
function save(key, value) {
try { localStorage.setItem(`${STORAGE_PREFIX}${key}`, String(value)); } catch { /* optional preference */ }
}
function stylesheetLinks() {
return [...document.querySelectorAll('link[rel="stylesheet"][href]')]
.map(link => ``)
.join("");
}
async function ensureSyntaxHighlight(root) {
const nodes = [...root.querySelectorAll('pre code[class*="language-"]:not(.language-mermaid)')];
if (!nodes.length) return;
let hljs;
try { hljs = await loadHighlight(); } catch { return; }
const highlightText = (text, language) => {
try {
return language
? hljs.highlight(text, { language, ignoreIllegals: true }).value
: hljs.highlightAuto(text).value;
} catch {
return hljs.highlightAuto(text).value;
}
};
nodes.forEach(node => {
const language = [...node.classList].find(name => name.startsWith("language-"))?.slice(9);
const lines = [...node.querySelectorAll(".code-line")];
if (lines.length) {
lines.forEach(line => {
if (line.querySelector('[class*="hljs-"]')) return;
line.innerHTML = highlightText(line.textContent || "", language);
});
node.classList.add("hljs");
return;
}
if (!node.querySelector('[class*="hljs-"]')) {
node.innerHTML = highlightText(node.textContent || "", language);
}
node.classList.add("hljs");
});
}
function mmToPx(doc) {
const probe = doc.createElement("div");
probe.style.cssText = "position:absolute;visibility:hidden;pointer-events:none;width:100mm;height:100mm;";
doc.body.appendChild(probe);
const pxPerMm = probe.getBoundingClientRect().height / 100;
probe.remove();
return pxPerMm;
}
function alignFooterToLastPage(doc, pageHeightMm, marginMm) {
const documentRoot = doc.querySelector(".pdf-document");
const spacer = doc.querySelector(".pdf-footer-spacer");
const footer = doc.querySelector(".pdf-generated-footer");
if (!documentRoot || !spacer || !footer) return;
spacer.style.height = "0px";
const pxPerMm = mmToPx(doc);
const pageContentHeight = (Number(pageHeightMm) - (2 * Number(marginMm))) * pxPerMm;
const footerHeight = footer.getBoundingClientRect().height;
if (!Number.isFinite(pageContentHeight) || pageContentHeight <= footerHeight || footerHeight <= 0) return;
const rootTop = documentRoot.getBoundingClientRect().top;
const footerTop = footer.getBoundingClientRect().top - rootTop;
const pageBreaks = documentRoot.querySelectorAll(".pdf-page-break");
const lastPageBreak = pageBreaks.length ? pageBreaks[pageBreaks.length - 1] : null;
const finalSegmentStart = lastPageBreak
? lastPageBreak.getBoundingClientRect().bottom - rootTop
: 0;
// A forced break starts a fresh page, so only the final segment determines
// where the one-time footer lands on the last printed page.
const finalSegmentHeight = Math.max(0, footerTop - finalSegmentStart);
const positionOnPage = ((finalSegmentHeight % pageContentHeight) + pageContentHeight) % pageContentHeight;
let spacerHeight = pageContentHeight - positionOnPage - footerHeight;
if (spacerHeight < -0.25) spacerHeight += pageContentHeight;
spacer.style.height = `${Math.max(0, spacerHeight)}px`;
}
function preparePreview(preview) {
const clone = preview.cloneNode(true);
clone.removeAttribute("id");
clone.classList.remove("preview");
clone.classList.add("pdf-content");
const computed = getComputedStyle(preview);
clone.style.fontFamily = computed.fontFamily;
clone.style.fontSize = computed.fontSize;
clone.style.lineHeight = computed.lineHeight;
clone.querySelectorAll('[contenteditable="true"]').forEach(node => node.removeAttribute("contenteditable"));
clone.querySelectorAll(".image-alias-tools, .image-alias-resize-handle").forEach(node => node.remove());
clone.querySelectorAll("details").forEach(node => { node.open = true; });
clone.querySelectorAll("input, button, select, textarea").forEach(node => { node.tabIndex = -1; });
if (clone.classList.contains("preview--raw")) {
clone.querySelectorAll(".preview-source-line").forEach(line => {
if (!/^---+$/.test(line.textContent.trim())) return;
const pageBreak = document.createElement("div");
pageBreak.className = "pdf-page-break";
pageBreak.setAttribute("aria-hidden", "true");
line.replaceWith(pageBreak);
});
}
clone.querySelectorAll("hr.page-break-marker, hr[data-page-break='true']").forEach(marker => {
const pageBreak = document.createElement("div");
pageBreak.className = "pdf-page-break";
pageBreak.setAttribute("aria-hidden", "true");
marker.replaceWith(pageBreak);
});
return clone;
}
function printStyles(cssSize, pageWidthMm, marginMm, preserveColors) {
return `
@page { size: ${cssSize}; margin: ${marginMm}mm; }
html { color-scheme: light; background: #fff !important; }
body { width: ${pageWidthMm - (2 * marginMm)}mm; margin: 0 !important; background: #fff !important; color: #16181d !important; }
.pdf-document { width: ${pageWidthMm - (2 * marginMm)}mm; box-sizing: border-box; }
.pdf-document-title { margin: 0 0 1.2em; padding: 0 0 .45em; border-bottom: 1px solid #d7dce2; }
.pdf-document-title h1 { margin: 0; font: 700 1.7rem/1.18 Inter, ui-sans-serif, system-ui, sans-serif; color: #111318; }
.pdf-footer-spacer {
display: block;
width: 100%;
height: 0;
margin: 0;
padding: 0;
border: 0;
}
.pdf-generated-footer {
box-sizing: border-box;
margin-top: 8mm;
padding-top: 1.5mm;
border-top: .2mm solid #e1e4e8;
color: #9299a3;
font: 500 7.5pt/1.2 Inter, ui-sans-serif, system-ui, sans-serif;
letter-spacing: .01em;
text-align: center;
break-inside: avoid;
page-break-inside: avoid;
}
.pdf-content { width: 100%; min-height: 0; margin: 0; padding: 0 !important; overflow: visible !important; color: #16181d !important; box-sizing: border-box; }
.pdf-content .preview-source-line::before { display: none !important; content: none !important; }
.pdf-content .preview-source-line { min-height: 0; }
.pdf-content .preview-editable:hover,
.pdf-content .preview-editable:focus { background: transparent !important; box-shadow: none !important; }
.pdf-content a { color: inherit !important; text-decoration: underline; }
${preserveColors ? `
.pdf-document,
.pdf-document * { print-color-adjust: exact !important; -webkit-print-color-adjust: exact !important; }
.pdf-content a { color: #5d50a5 !important; }
.pdf-content code:not(pre code) { background: #eeeaf7 !important; border-color: #bcb2a6 !important; color: #403a33 !important; }
.pdf-content pre { background: #efeae2 !important; border-color: #e2dcd3 !important; color: #413b34 !important; }
.pdf-content pre code,
.pdf-content .hljs { background: transparent !important; color: #403a33 !important; }
.pdf-content .hljs-keyword,
.pdf-content .hljs-selector-tag,
.pdf-content .hljs-literal,
.pdf-content .hljs-section,
.pdf-content .hljs-link { color: #7046a5 !important; }
.pdf-content .hljs-string,
.pdf-content .hljs-attr,
.pdf-content .hljs-regexp,
.pdf-content .hljs-template-tag,
.pdf-content .hljs-template-variable { color: #36704a !important; }
.pdf-content .hljs-number,
.pdf-content .hljs-symbol,
.pdf-content .hljs-bullet { color: #a54c25 !important; }
.pdf-content .hljs-title,
.pdf-content .hljs-title.class_,
.pdf-content .hljs-title.function_,
.pdf-content .hljs-built_in,
.pdf-content .hljs-type { color: #3568b8 !important; }
.pdf-content .hljs-variable,
.pdf-content .hljs-selector-id,
.pdf-content .hljs-selector-class,
.pdf-content .hljs-property,
.pdf-content .hljs-attribute { color: #9b3443 !important; }
.pdf-content .hljs-comment,
.pdf-content .hljs-quote { color: #756c61 !important; font-style: italic; }
.pdf-content .hljs-meta { color: #7e5d1d !important; }
.pdf-content blockquote { border-left-color: #6859b5 !important; color: #625a50 !important; }
.pdf-content mark { background: #f4e7be !important; color: #624700 !important; }
.pdf-content th { background: #f5f1ea !important; color: #29251f !important; }
.pdf-content .markdown-alert--success { border-color: #4c8064 !important; background: rgba(61, 121, 88, .10) !important; }
.pdf-content .markdown-alert--info { border-color: #56759c !important; background: rgba(68, 103, 148, .09) !important; }
.pdf-content .markdown-alert--warning { border-color: #a27831 !important; background: rgba(155, 112, 35, .11) !important; }
.pdf-content .markdown-alert--danger { border-color: #a54b55 !important; background: rgba(155, 52, 67, .09) !important; }
.pdf-content .task-checkbox { accent-color: #6859b5 !important; }
` : ""}
.pdf-content pre { overflow: visible !important; white-space: pre-wrap !important; break-inside: avoid; page-break-inside: avoid; }
.pdf-content pre code,
.pdf-content pre.code-with-lines .code-line { white-space: pre-wrap !important; overflow-wrap: anywhere; word-break: break-word; }
.pdf-content table,
.pdf-content img,
.pdf-content svg,
.pdf-content blockquote { break-inside: avoid; page-break-inside: avoid; }
.pdf-content img,
.pdf-content svg { max-width: 100% !important; height: auto; }
.pdf-content .table-wrap { overflow: visible !important; }
.pdf-page-break { display: block; height: 0; margin: 0; padding: 0; border: 0; break-after: page; page-break-after: always; }
`;
}
function openPrintWindow({ preview, title, formatKey, marginMm, includeTitle, preserveColors }) {
const format = PAGE_FORMATS[formatKey] || PAGE_FORMATS["a4-portrait"];
const safeMargin = [8, 12, 20].includes(Number(marginMm)) ? Number(marginMm) : 12;
const printWindow = window.open("", "_blank");
if (!printWindow) throw new Error(t("editor.pdfPopupBlocked", {}, "The PDF window was blocked by the browser."));
try { printWindow.opener = null; } catch { /* same-origin hardening is best effort */ }
const content = preparePreview(preview);
const titleBlock = includeTitle && title
? `${escapeHtml(title)}