hotfix pagebreak
This commit is contained in:
Generated
+1
-1
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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();
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
Reference in New Issue
Block a user