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
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"
+78
View File
@@ -0,0 +1,78 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="/static/css/styles.css">
<link rel="stylesheet" href="/static/libs/rustpad-player/player.css">
<script>window.__RUSTPAD_CONFIG__={assetVersion:"test"};</script>
<script type="importmap">
{"imports":{
"@rustpad/i18n":"/static/js/i18n.js",
"@rustpad/toast":"/static/js/toast.js",
"@rustpad/vendor-libs":"/static/js/vendor-libs.js",
"@rustpad/markdown":"/static/js/markdown.js",
"@rustpad/emoji-data":"/static/js/emoji-data.js",
"@rustpad/image-alias":"/static/js/image-alias.js"
}}
</script>
</head>
<body>
<div id="toast"></div>
<button id="pdf">PDF</button>
<dialog id="dlg">
<form id="form" method="dialog">
<select id="pdf-page-format"><option value="screen-16-9" selected>screen</option></select>
<select id="pdf-page-margin"><option value="12" selected>12</option></select>
<input id="pdf-include-title" type="checkbox">
<input id="pdf-preserve-colors" type="checkbox">
<button data-pdf-cancel type="button">cancel</button>
<button type="submit">go</button>
</form>
</dialog>
<div id="preview" class="preview markdown-body"></div>
<script type="module">
import { renderMarkdown } from "@rustpad/markdown";
import { bindPdfExport } from "/static/js/pdf-export.js";
const source = await (await fetch('/TEST_NOTE_FEATURES.txt')).text();
const preview = document.querySelector('#preview');
preview.innerHTML = renderMarkdown(source);
// Replace raw Mermaid source blocks with representative SVGs so pagination
// exercises the same tall/wide atomic visuals as the real rendered preview.
[...preview.querySelectorAll('.mermaid')].forEach((node, index) => {
const tall = index === 2 || index === 3 || index === 4;
const height = tall ? 1500 : 520;
node.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 ${height}" width="900" height="${height}"><rect width="900" height="${height}" fill="#efeae2"/><rect x="90" y="60" width="720" height="180" rx="16" fill="#202323"/><text x="450" y="160" text-anchor="middle" font-size="56" fill="white">Mermaid ${index + 1}</text><line x1="450" y1="240" x2="450" y2="${height - 100}" stroke="#777" stroke-width="6"/></svg>`;
});
let popupFrame = null;
window.open = () => {
popupFrame = document.createElement('iframe');
popupFrame.id = 'print-frame';
popupFrame.style.cssText = 'position:absolute;left:-10000px;top:0;width:1400px;height:900px;border:0';
document.body.appendChild(popupFrame);
const win = popupFrame.contentWindow;
win.focus = () => {};
win.print = async () => {
const html = win.document.documentElement.outerHTML;
await fetch('/capture', { method: 'POST', headers: {'Content-Type':'text/html'}, body: html });
document.documentElement.dataset.captureDone = '1';
};
return win;
};
const dialog = document.querySelector('#dlg');
bindPdfExport({
button: document.querySelector('#pdf'),
dialog,
form: document.querySelector('#form'),
preview,
title: () => 'TEST_NOTE_FEATURES',
});
dialog.showModal();
document.querySelector('#form').dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
</script>
</body>
</html>
File diff suppressed because one or more lines are too long
+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 });