hotfix pagebreak
This commit is contained in:
+135
-2
@@ -9,7 +9,7 @@
|
||||
|
||||
import { t } from "@rustpad/i18n";
|
||||
import { toast } from "@rustpad/toast";
|
||||
import { loadHighlight } from "@rustpad/vendor-libs";
|
||||
import { loadHighlight, loadMermaid } from "@rustpad/vendor-libs";
|
||||
|
||||
const STORAGE_PREFIX = "rustpad:pdf-export:";
|
||||
|
||||
@@ -122,6 +122,126 @@ async function waitForPrintableLayout(doc) {
|
||||
await nextFrame(doc.defaultView);
|
||||
}
|
||||
|
||||
function isBlankPrintLine(node) {
|
||||
if (!(node instanceof Element) || !node.classList.contains("preview-source-line")) return false;
|
||||
if (node.textContent.trim()) return false;
|
||||
return node.children.length === 1 && node.firstElementChild?.tagName === "BR";
|
||||
}
|
||||
|
||||
function collapseHeadingGaps(root) {
|
||||
root.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach(heading => {
|
||||
let next = heading.nextElementSibling;
|
||||
while (isBlankPrintLine(next)) {
|
||||
const remove = next;
|
||||
next = next.nextElementSibling;
|
||||
remove.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function directPrintBlock(root, node) {
|
||||
let block = node;
|
||||
while (block?.parentElement && block.parentElement !== root) block = block.parentElement;
|
||||
return block?.parentElement === root ? block : null;
|
||||
}
|
||||
|
||||
function shortLeadBlock(node) {
|
||||
if (!node) return false;
|
||||
if (node.matches("h1, h2, h3, h4, h5, h6")) return true;
|
||||
if (!node.matches("p")) return false;
|
||||
const text = node.textContent.trim();
|
||||
return text.length > 0 && text.length <= 320;
|
||||
}
|
||||
|
||||
function previousNonBlank(node) {
|
||||
let current = node?.previousElementSibling || null;
|
||||
while (isBlankPrintLine(current)) current = current.previousElementSibling;
|
||||
return current;
|
||||
}
|
||||
|
||||
function groupAtomicVisuals(root) {
|
||||
const blocks = new Set();
|
||||
root.querySelectorAll(".mermaid, .rustpad-media").forEach(node => {
|
||||
const block = directPrintBlock(root, node);
|
||||
if (block) blocks.add(block);
|
||||
});
|
||||
root.querySelectorAll("img").forEach(node => {
|
||||
const block = directPrintBlock(root, node);
|
||||
if (block && !block.textContent.trim()) blocks.add(block);
|
||||
});
|
||||
|
||||
for (const block of blocks) {
|
||||
if (block.parentElement !== root) continue;
|
||||
block.classList.add("pdf-atomic-visual");
|
||||
|
||||
let first = block;
|
||||
const lead = previousNonBlank(block);
|
||||
if (shortLeadBlock(lead)) {
|
||||
first = lead;
|
||||
if (lead.matches("p")) {
|
||||
const heading = previousNonBlank(lead);
|
||||
if (heading?.matches("h1, h2, h3, h4, h5, h6")) first = heading;
|
||||
}
|
||||
}
|
||||
|
||||
if (first === block) continue;
|
||||
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "pdf-keep-unit";
|
||||
root.insertBefore(wrapper, first);
|
||||
|
||||
let current = first;
|
||||
while (current) {
|
||||
const next = current.nextElementSibling;
|
||||
if (isBlankPrintLine(current)) current.remove();
|
||||
else wrapper.appendChild(current);
|
||||
if (current === block) break;
|
||||
current = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function preparePrintPagination(root) {
|
||||
collapseHeadingGaps(root);
|
||||
groupAtomicVisuals(root);
|
||||
}
|
||||
|
||||
async function ensurePrintableMermaid(root) {
|
||||
const nodes = [...root.querySelectorAll(".mermaid[data-mermaid-source]")];
|
||||
if (!nodes.length) return;
|
||||
|
||||
const originals = nodes.map(node => ({
|
||||
node,
|
||||
html: node.innerHTML,
|
||||
processed: node.getAttribute("data-processed"),
|
||||
}));
|
||||
const staging = document.createElement("div");
|
||||
staging.setAttribute("aria-hidden", "true");
|
||||
staging.style.cssText = "position:fixed;left:-200vw;top:0;width:1200px;visibility:hidden;pointer-events:none;z-index:-1";
|
||||
|
||||
try {
|
||||
const mermaid = await loadMermaid();
|
||||
nodes.forEach(node => {
|
||||
node.removeAttribute("data-processed");
|
||||
node.textContent = node.dataset.mermaidSource || "";
|
||||
});
|
||||
|
||||
staging.appendChild(root);
|
||||
document.body.appendChild(staging);
|
||||
mermaid.initialize({ startOnLoad: false, theme: "default", securityLevel: "strict" });
|
||||
await mermaid.run({ nodes });
|
||||
} catch {
|
||||
originals.forEach(({ node, html, processed }) => {
|
||||
node.innerHTML = html;
|
||||
if (processed === null) node.removeAttribute("data-processed");
|
||||
else node.setAttribute("data-processed", processed);
|
||||
});
|
||||
} finally {
|
||||
if (root.parentElement === staging) staging.removeChild(root);
|
||||
staging.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function measureLastPageFill(doc, documentRoot, pageContentHeight) {
|
||||
const content = documentRoot.querySelector(":scope > .pdf-content");
|
||||
if (!content) return null;
|
||||
@@ -254,13 +374,14 @@ function preparePreview(preview) {
|
||||
marker.replaceWith(pageBreak);
|
||||
});
|
||||
|
||||
preparePrintPagination(clone);
|
||||
return clone;
|
||||
}
|
||||
|
||||
function printStyles(cssSize, pageWidthMm, pageHeightMm, marginMm, preserveColors) {
|
||||
const pageContentWidthMm = Math.max(20, Number(pageWidthMm) - (2 * Number(marginMm)));
|
||||
const pageContentHeightMm = Math.max(20, Number(pageHeightMm) - (2 * Number(marginMm)));
|
||||
const atomicMaxHeightMm = Math.max(20, pageContentHeightMm - 8);
|
||||
const atomicMaxHeightMm = Math.max(20, pageContentHeightMm - 20);
|
||||
const mediaMaxWidthMm = atomicMaxHeightMm * (16 / 9);
|
||||
|
||||
return `
|
||||
@@ -396,6 +517,17 @@ function printStyles(cssSize, pageWidthMm, pageHeightMm, marginMm, preserveColor
|
||||
-webkit-box-decoration-break: clone;
|
||||
}
|
||||
|
||||
.pdf-content .pdf-keep-unit {
|
||||
break-inside: avoid-page;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
.pdf-content .pdf-keep-unit > :first-child { margin-top: 0 !important; }
|
||||
.pdf-content .pdf-keep-unit > :last-child { margin-bottom: 0 !important; }
|
||||
.pdf-content .pdf-atomic-visual {
|
||||
break-before: avoid-page;
|
||||
page-break-before: avoid;
|
||||
}
|
||||
|
||||
/* Atomic visual items are scaled to the printable page instead of being fragmented. */
|
||||
.pdf-content img {
|
||||
max-width: 100% !important;
|
||||
@@ -517,6 +649,7 @@ function openPrintWindow({ preview, title, formatKey, marginMm, includeTitle, pr
|
||||
const footerText = escapeHtml(t("editor.pdfGeneratedFooter", {}, "Generated by RustPad"));
|
||||
|
||||
return (async () => {
|
||||
await ensurePrintableMermaid(content);
|
||||
if (preserveColors) await ensureSyntaxHighlight(content);
|
||||
|
||||
printWindow.document.open();
|
||||
|
||||
Reference in New Issue
Block a user