hotfix pagebreak

This commit is contained in:
Mateusz Gruszczyński
2026-09-12 09:47:21 +02:00
parent ba0c2a84d5
commit 8df9b5d092
7 changed files with 245 additions and 4 deletions
+3
View File
@@ -409,6 +409,9 @@ export function startNoteEditor(adapter) {
const nodes = [...preview.querySelectorAll(".mermaid")];
if (!nodes.length) return;
try {
nodes.forEach(node => {
if (!node.dataset.mermaidSource) node.dataset.mermaidSource = node.textContent || "";
});
const mermaid = await loadMermaid();
mermaid.initialize({ startOnLoad: false, theme: getTheme() === "dark" ? "dark" : "default", securityLevel: "strict" });
await mermaid.run({ nodes });
+135 -2
View File
@@ -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();
+3
View File
@@ -38,6 +38,9 @@ async function renderMermaid() {
const nodes = [...content.querySelectorAll(".mermaid")];
if (!nodes.length) return;
try {
nodes.forEach(node => {
if (!node.dataset.mermaidSource) node.dataset.mermaidSource = node.textContent || "";
});
const mermaid = await loadMermaid();
mermaid.initialize({ startOnLoad: false, theme: getTheme() === "dark" ? "dark" : "default", securityLevel: "strict" });
await mermaid.run({ nodes });