Compare commits

..
2 Commits
Author SHA1 Message Date
Mateusz Gruszczyński f16a860f0a hotfix pagebreak 2026-09-12 09:48:28 +02:00
Mateusz Gruszczyński 8df9b5d092 hotfix pagebreak 2026-09-12 09:47:21 +02:00
5 changed files with 143 additions and 4 deletions
Generated
+1 -1
View File
@@ -2581,7 +2581,7 @@ dependencies = [
[[package]] [[package]]
name = "rustpad" name = "rustpad"
version = "0.2.72" version = "0.2.73"
dependencies = [ dependencies = [
"argon2", "argon2",
"aws-config", "aws-config",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "rustpad" name = "rustpad"
version = "0.2.72" version = "0.2.73"
edition = "2024" edition = "2024"
rust-version = "1.94" rust-version = "1.94"
description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL" description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL"
+3
View File
@@ -409,6 +409,9 @@ export function startNoteEditor(adapter) {
const nodes = [...preview.querySelectorAll(".mermaid")]; const nodes = [...preview.querySelectorAll(".mermaid")];
if (!nodes.length) return; if (!nodes.length) return;
try { try {
nodes.forEach(node => {
if (!node.dataset.mermaidSource) node.dataset.mermaidSource = node.textContent || "";
});
const mermaid = await loadMermaid(); const mermaid = await loadMermaid();
mermaid.initialize({ startOnLoad: false, theme: getTheme() === "dark" ? "dark" : "default", securityLevel: "strict" }); mermaid.initialize({ startOnLoad: false, theme: getTheme() === "dark" ? "dark" : "default", securityLevel: "strict" });
await mermaid.run({ nodes }); await mermaid.run({ nodes });
+135 -2
View File
@@ -9,7 +9,7 @@
import { t } from "@rustpad/i18n"; import { t } from "@rustpad/i18n";
import { toast } from "@rustpad/toast"; import { toast } from "@rustpad/toast";
import { loadHighlight } from "@rustpad/vendor-libs"; import { loadHighlight, loadMermaid } from "@rustpad/vendor-libs";
const STORAGE_PREFIX = "rustpad:pdf-export:"; const STORAGE_PREFIX = "rustpad:pdf-export:";
@@ -122,6 +122,126 @@ async function waitForPrintableLayout(doc) {
await nextFrame(doc.defaultView); 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) { function measureLastPageFill(doc, documentRoot, pageContentHeight) {
const content = documentRoot.querySelector(":scope > .pdf-content"); const content = documentRoot.querySelector(":scope > .pdf-content");
if (!content) return null; if (!content) return null;
@@ -254,13 +374,14 @@ function preparePreview(preview) {
marker.replaceWith(pageBreak); marker.replaceWith(pageBreak);
}); });
preparePrintPagination(clone);
return clone; return clone;
} }
function printStyles(cssSize, pageWidthMm, pageHeightMm, marginMm, preserveColors) { function printStyles(cssSize, pageWidthMm, pageHeightMm, marginMm, preserveColors) {
const pageContentWidthMm = Math.max(20, Number(pageWidthMm) - (2 * Number(marginMm))); const pageContentWidthMm = Math.max(20, Number(pageWidthMm) - (2 * Number(marginMm)));
const pageContentHeightMm = Math.max(20, Number(pageHeightMm) - (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); const mediaMaxWidthMm = atomicMaxHeightMm * (16 / 9);
return ` return `
@@ -396,6 +517,17 @@ function printStyles(cssSize, pageWidthMm, pageHeightMm, marginMm, preserveColor
-webkit-box-decoration-break: clone; -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. */ /* Atomic visual items are scaled to the printable page instead of being fragmented. */
.pdf-content img { .pdf-content img {
max-width: 100% !important; 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")); const footerText = escapeHtml(t("editor.pdfGeneratedFooter", {}, "Generated by RustPad"));
return (async () => { return (async () => {
await ensurePrintableMermaid(content);
if (preserveColors) await ensureSyntaxHighlight(content); if (preserveColors) await ensureSyntaxHighlight(content);
printWindow.document.open(); printWindow.document.open();
+3
View File
@@ -38,6 +38,9 @@ async function renderMermaid() {
const nodes = [...content.querySelectorAll(".mermaid")]; const nodes = [...content.querySelectorAll(".mermaid")];
if (!nodes.length) return; if (!nodes.length) return;
try { try {
nodes.forEach(node => {
if (!node.dataset.mermaidSource) node.dataset.mermaidSource = node.textContent || "";
});
const mermaid = await loadMermaid(); const mermaid = await loadMermaid();
mermaid.initialize({ startOnLoad: false, theme: getTheme() === "dark" ? "dark" : "default", securityLevel: "strict" }); mermaid.initialize({ startOnLoad: false, theme: getTheme() === "dark" ? "dark" : "default", securityLevel: "strict" });
await mermaid.run({ nodes }); await mermaid.run({ nodes });