v0.2.95
This commit is contained in:
1 parent
25d04daaee
commit
b15a6c54ce
8 files changed
+213
-51
No files matched your search
Generated
+1
-1
@@ -2674,7 +2674,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustpad"
|
name = "rustpad"
|
||||||
version = "0.2.94"
|
version = "0.2.95"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"aws-config",
|
"aws-config",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustpad"
|
name = "rustpad"
|
||||||
version = "0.2.94"
|
version = "0.2.95"
|
||||||
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"
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
RustPad is a collaborative Markdown editor with standalone notes and workspaces.
|
RustPad is a collaborative Markdown editor with standalone notes and workspaces.
|
||||||
|
|
||||||
|
## Release 0.2.95 — dark PDF export
|
||||||
|
|
||||||
|
- PDF export now includes a dark-mode checkbox enabled by default.
|
||||||
|
- Exported PDFs use the selected light/dark mode consistently for content, syntax highlighting, and Mermaid diagrams; colors and backgrounds are preserved automatically.
|
||||||
|
- PDF margins are selectable again (0 / 8 / 12 / 20 mm) and are applied to every page format, including 16:9 and 4:3 presentation pages.
|
||||||
|
- PDF pagination keeps short lists, short code blocks, and heading lead-ins together when possible to avoid awkward splits between pages.
|
||||||
|
|
||||||
## Release 0.2.94 — authentication session handling
|
## Release 0.2.94 — authentication session handling
|
||||||
|
|
||||||
- `GET /api/auth/session` checks the visitor's login state: `200 {"authenticated":false,"session":null}` for guests, missing/expired cookies, or logged-out users; `200 {"authenticated":true,"session":{...}}` for a valid account session. The response is never cached. Expired cookies are cleared; valid session cookies are refreshed as before.
|
- `GET /api/auth/session` checks the visitor's login state: `200 {"authenticated":false,"session":null}` for guests, missing/expired cookies, or logged-out users; `200 {"authenticated":true,"session":{...}}` for a valid account session. The response is never cached. Expired cookies are cleared; valid session cookies are refreshed as before.
|
||||||
@@ -71,7 +78,7 @@ The selected interface language is stored in the browser and can be changed from
|
|||||||
|
|
||||||
## PDF export and page breaks
|
## PDF export and page breaks
|
||||||
|
|
||||||
Use the **PDF** button in the editor header to choose the page format and margins, then generate a browser-native PDF print view. The export supports A5/A4/A3, Letter, 16:9, and 4:3 layouts. Enable **Preserve colors and backgrounds** to keep syntax highlighting, alerts, quotes, highlighted text, table headers, inline code, links, and other styled Markdown elements. The exporter re-applies Highlight.js to the print copy and uses a print-safe light color palette so code colors do not depend on the editor theme. The end of the exported document includes one subtle localized RustPad footer.
|
Use the **PDF** button in the editor header to choose the page format and margins, then generate a browser-native PDF print view. The export supports A5/A4/A3, Letter, 16:9, and 4:3 layouts. Syntax highlighting, alerts, quotes, highlighted text, table headers, inline code, links, and other styled Markdown elements keep their colors and backgrounds automatically. The exporter re-applies Highlight.js to the print copy and uses print-safe light/dark palettes so code colors do not depend on the editor theme. The end of the exported document includes one subtle localized RustPad footer.
|
||||||
|
|
||||||
A Markdown line containing only `[PAGEBREAK]` creates an explicit page break during PDF export. Insert it from **More → Page break (PDF)** or with `Ctrl/Cmd+Enter`. A normal `---` remains a Markdown horizontal rule and no longer forces a new PDF page.
|
A Markdown line containing only `[PAGEBREAK]` creates an explicit page break during PDF export. Insert it from **More → Page break (PDF)** or with `Ctrl/Cmd+Enter`. A normal `---` remains a Markdown horizontal rule and no longer forces a new PDF page.
|
||||||
|
|
||||||
|
|||||||
+1
-2
@@ -388,8 +388,7 @@
|
|||||||
"editor.pageBreakShortcut": "Page break (PDF) · Ctrl/Cmd+Enter",
|
"editor.pageBreakShortcut": "Page break (PDF) · Ctrl/Cmd+Enter",
|
||||||
"editor.pageFormat": "Page format",
|
"editor.pageFormat": "Page format",
|
||||||
"editor.pdfPageBreakHelp": "Use Page break (PDF) or Ctrl/Cmd+Enter to start a new PDF page.",
|
"editor.pdfPageBreakHelp": "Use Page break (PDF) or Ctrl/Cmd+Enter to start a new PDF page.",
|
||||||
"editor.preservePdfColors": "Preserve colors and backgrounds",
|
"editor.darkPdf": "Dark PDF",
|
||||||
"editor.pdfColorHelp": "Keeps syntax highlighting, alerts, quotes, highlights, table headers, and other colors in the PDF.",
|
|
||||||
"editor.pdfPrintHelp": "The browser print dialog will open. Choose Save as PDF.",
|
"editor.pdfPrintHelp": "The browser print dialog will open. Choose Save as PDF.",
|
||||||
"editor.pdfGeneratedFooter": "Generated by RustPad",
|
"editor.pdfGeneratedFooter": "Generated by RustPad",
|
||||||
"editor.presentation16x9": "16:9 — Presentation",
|
"editor.presentation16x9": "16:9 — Presentation",
|
||||||
|
|||||||
+1
-2
@@ -388,8 +388,7 @@
|
|||||||
"editor.pageBreakShortcut": "Podział strony (PDF) · Ctrl/Cmd+Enter",
|
"editor.pageBreakShortcut": "Podział strony (PDF) · Ctrl/Cmd+Enter",
|
||||||
"editor.pageFormat": "Format strony",
|
"editor.pageFormat": "Format strony",
|
||||||
"editor.pdfPageBreakHelp": "Użyj opcji Podział strony (PDF) lub Ctrl/Cmd+Enter, aby rozpocząć nową stronę PDF.",
|
"editor.pdfPageBreakHelp": "Użyj opcji Podział strony (PDF) lub Ctrl/Cmd+Enter, aby rozpocząć nową stronę PDF.",
|
||||||
"editor.preservePdfColors": "Zachowaj kolory i tła",
|
"editor.darkPdf": "Ciemny PDF",
|
||||||
"editor.pdfColorHelp": "Zachowuje w PDF kolorowanie składni, alerty, cytaty, wyróżnienia, nagłówki tabel i inne kolory.",
|
|
||||||
"editor.pdfPrintHelp": "Otworzy się okno drukowania przeglądarki. Wybierz Zapisz jako PDF.",
|
"editor.pdfPrintHelp": "Otworzy się okno drukowania przeglądarki. Wybierz Zapisz jako PDF.",
|
||||||
"editor.pdfGeneratedFooter": "Wygenerowano przez RustPad",
|
"editor.pdfGeneratedFooter": "Wygenerowano przez RustPad",
|
||||||
"editor.presentation16x9": "16:9 — prezentacja",
|
"editor.presentation16x9": "16:9 — prezentacja",
|
||||||
|
|||||||
@@ -9186,7 +9186,7 @@ dialog::backdrop {
|
|||||||
|
|
||||||
.pdf-export-fields {
|
.pdf-export-fields {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(150px, .62fr);
|
grid-template-columns: 1fr;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9209,22 +9209,17 @@ dialog::backdrop {
|
|||||||
gap: 9px;
|
gap: 9px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pdf-export-color-toggle,
|
.pdf-export-dark-toggle,
|
||||||
.pdf-export-title-toggle {
|
.pdf-export-title-toggle {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pdf-export-color-help,
|
|
||||||
.pdf-export-help {
|
.pdf-export-help {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: .78rem;
|
font-size: .78rem;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pdf-export-color-help {
|
|
||||||
margin: -6px 0 0 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#pdf-export-dialog .dialog-actions .primary-button {
|
#pdf-export-dialog .dialog-actions .primary-button {
|
||||||
width: auto;
|
width: auto;
|
||||||
min-width: 132px;
|
min-width: 132px;
|
||||||
|
|||||||
+3
-5
@@ -316,20 +316,18 @@
|
|||||||
<option value="screen-4-3">4:3 — Presentation</option>
|
<option value="screen-4-3">4:3 — Presentation</option>
|
||||||
</select></label>
|
</select></label>
|
||||||
<label><span>Margins</span><select class="app-select-input" id="pdf-page-margin">
|
<label><span>Margins</span><select class="app-select-input" id="pdf-page-margin">
|
||||||
|
<option value="0">None (0 mm)</option>
|
||||||
<option value="8">Narrow (8 mm)</option>
|
<option value="8">Narrow (8 mm)</option>
|
||||||
<option value="12" selected>Normal (12 mm)</option>
|
<option value="12" selected>Normal (12 mm)</option>
|
||||||
<option value="20">Wide (20 mm)</option>
|
<option value="20">Wide (20 mm)</option>
|
||||||
</select></label>
|
</select></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="pdf-export-options">
|
<div class="pdf-export-options">
|
||||||
|
<label class="public-task-toggle pdf-export-dark-toggle"><input id="pdf-dark-mode" type="checkbox"
|
||||||
|
checked> <span data-i18n="editor.darkPdf">Dark PDF</span></label>
|
||||||
<label class="public-task-toggle pdf-export-title-toggle"><input id="pdf-include-title" type="checkbox">
|
<label class="public-task-toggle pdf-export-title-toggle"><input id="pdf-include-title" type="checkbox">
|
||||||
Include document title</label>
|
Include document title</label>
|
||||||
<label class="public-task-toggle pdf-export-color-toggle"><input id="pdf-preserve-colors"
|
|
||||||
type="checkbox"> <span data-i18n="editor.preservePdfColors">Preserve colors and
|
|
||||||
backgrounds</span></label>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="pdf-export-color-help" data-i18n="editor.pdfColorHelp">Keeps syntax highlighting, alerts, quotes,
|
|
||||||
highlights, table headers, and other colors in the PDF.</p>
|
|
||||||
<p class="pdf-export-help">The browser print dialog will open. Choose Save as PDF.</p>
|
<p class="pdf-export-help">The browser print dialog will open. Choose Save as PDF.</p>
|
||||||
<div class="dialog-actions"><button class="secondary-button" value="cancel" type="button"
|
<div class="dialog-actions"><button class="secondary-button" value="cancel" type="button"
|
||||||
data-pdf-cancel>Cancel</button><button id="generate-pdf" class="primary-button"
|
data-pdf-cancel>Cancel</button><button id="generate-pdf" class="primary-button"
|
||||||
|
|||||||
+196
-32
@@ -201,12 +201,79 @@ function groupAtomicVisuals(root) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function preparePrintPagination(root) {
|
function isShortList(node) {
|
||||||
collapseHeadingGaps(root);
|
if (!node?.matches?.("ul, ol")) return false;
|
||||||
groupAtomicVisuals(root);
|
const items = [...node.children].filter(child => child.matches?.("li"));
|
||||||
|
if (!items.length || items.length > 8) return false;
|
||||||
|
if (node.querySelector(":scope > li > ul, :scope > li > ol")) return false;
|
||||||
|
return node.textContent.trim().length <= 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensurePrintableMermaid(root) {
|
function isShortCodeBlock(node) {
|
||||||
|
if (!node?.matches?.("pre")) return false;
|
||||||
|
const text = node.textContent || "";
|
||||||
|
const lines = text ? text.split(/\r?\n/).length : 0;
|
||||||
|
return lines > 0 && lines <= 12 && text.length <= 1400;
|
||||||
|
}
|
||||||
|
|
||||||
|
function markShortKeepBlocks(root) {
|
||||||
|
root.querySelectorAll("ul, ol").forEach(list => {
|
||||||
|
if (isShortList(list)) list.classList.add("pdf-keep-short");
|
||||||
|
});
|
||||||
|
root.querySelectorAll("pre").forEach(pre => {
|
||||||
|
if (isShortCodeBlock(pre)) pre.classList.add("pdf-keep-short");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHeadingLead(node) {
|
||||||
|
if (!node || node.classList?.contains("pdf-page-break")) return false;
|
||||||
|
if (node.matches?.("p, blockquote")) {
|
||||||
|
const text = node.textContent.trim();
|
||||||
|
return text.length > 0 && text.length <= 520;
|
||||||
|
}
|
||||||
|
return isShortList(node) || isShortCodeBlock(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupHeadingsWithLead(root) {
|
||||||
|
const headings = [...root.querySelectorAll(":scope > h1, :scope > h2, :scope > h3, :scope > h4, :scope > h5, :scope > h6")];
|
||||||
|
|
||||||
|
headings.forEach(heading => {
|
||||||
|
if (heading.parentElement !== root) return;
|
||||||
|
|
||||||
|
let lead = heading.nextElementSibling;
|
||||||
|
while (isBlankPrintLine(lead)) {
|
||||||
|
const remove = lead;
|
||||||
|
lead = lead.nextElementSibling;
|
||||||
|
remove.remove();
|
||||||
|
}
|
||||||
|
if (!isHeadingLead(lead)) return;
|
||||||
|
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.className = "pdf-keep-unit pdf-heading-lead";
|
||||||
|
root.insertBefore(wrapper, heading);
|
||||||
|
wrapper.appendChild(heading);
|
||||||
|
wrapper.appendChild(lead);
|
||||||
|
|
||||||
|
if (lead.matches?.("p, blockquote")) {
|
||||||
|
let next = wrapper.nextElementSibling;
|
||||||
|
while (isBlankPrintLine(next)) {
|
||||||
|
const remove = next;
|
||||||
|
next = next.nextElementSibling;
|
||||||
|
remove.remove();
|
||||||
|
}
|
||||||
|
if (isShortList(next) || isShortCodeBlock(next)) wrapper.appendChild(next);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function preparePrintPagination(root) {
|
||||||
|
collapseHeadingGaps(root);
|
||||||
|
markShortKeepBlocks(root);
|
||||||
|
groupAtomicVisuals(root);
|
||||||
|
groupHeadingsWithLead(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensurePrintableMermaid(root, darkMode = false) {
|
||||||
const nodes = [...root.querySelectorAll(".mermaid[data-mermaid-source]")];
|
const nodes = [...root.querySelectorAll(".mermaid[data-mermaid-source]")];
|
||||||
if (!nodes.length) return;
|
if (!nodes.length) return;
|
||||||
|
|
||||||
@@ -228,7 +295,7 @@ async function ensurePrintableMermaid(root) {
|
|||||||
|
|
||||||
staging.appendChild(root);
|
staging.appendChild(root);
|
||||||
document.body.appendChild(staging);
|
document.body.appendChild(staging);
|
||||||
mermaid.initialize({ startOnLoad: false, theme: "default", securityLevel: "strict" });
|
mermaid.initialize({ startOnLoad: false, theme: darkMode ? "dark" : "default", securityLevel: "strict" });
|
||||||
await mermaid.run({ nodes });
|
await mermaid.run({ nodes });
|
||||||
} catch {
|
} catch {
|
||||||
originals.forEach(({ node, html, processed }) => {
|
originals.forEach(({ node, html, processed }) => {
|
||||||
@@ -378,25 +445,46 @@ function preparePreview(preview) {
|
|||||||
return clone;
|
return clone;
|
||||||
}
|
}
|
||||||
|
|
||||||
function printStyles(cssSize, pageWidthMm, pageHeightMm, marginMm, preserveColors) {
|
function printStyles(cssSize, pageWidthMm, pageHeightMm, marginMm, darkMode) {
|
||||||
const pageContentWidthMm = Math.max(20, Number(pageWidthMm) - (2 * Number(marginMm)));
|
const safeMargin = Math.max(0, Number(marginMm) || 0);
|
||||||
const pageContentHeightMm = Math.max(20, Number(pageHeightMm) - (2 * Number(marginMm)));
|
const pageContentWidthMm = Math.max(20, Number(pageWidthMm) - (2 * safeMargin));
|
||||||
|
const pageContentHeightMm = Math.max(20, Number(pageHeightMm) - (2 * safeMargin));
|
||||||
const atomicMaxHeightMm = Math.max(20, pageContentHeightMm - 20);
|
const atomicMaxHeightMm = Math.max(20, pageContentHeightMm - 20);
|
||||||
const mediaMaxWidthMm = atomicMaxHeightMm * (16 / 9);
|
const mediaMaxWidthMm = atomicMaxHeightMm * (16 / 9);
|
||||||
|
const pageBackground = darkMode ? "#15181d" : "#fff";
|
||||||
|
const textColor = darkMode ? "#e6e9ed" : "#16181d";
|
||||||
|
const headingColor = darkMode ? "#f1f3f5" : "#111318";
|
||||||
|
const borderColor = darkMode ? "#3a414b" : "#d7dce2";
|
||||||
|
const footerColor = darkMode ? "#aeb5bf" : "#9299a3";
|
||||||
|
const colorScheme = darkMode ? "dark" : "light";
|
||||||
|
|
||||||
return `
|
return `
|
||||||
@page { size: ${cssSize}; margin: ${marginMm}mm; }
|
@page { size: ${cssSize}; margin: ${safeMargin}mm; background: ${pageBackground}; }
|
||||||
html { color-scheme: light; background: #fff !important; }
|
html {
|
||||||
|
color-scheme: ${colorScheme};
|
||||||
|
background: ${pageBackground} !important;
|
||||||
|
print-color-adjust: exact !important;
|
||||||
|
-webkit-print-color-adjust: exact !important;
|
||||||
|
}
|
||||||
body {
|
body {
|
||||||
width: ${pageContentWidthMm}mm;
|
width: ${pageContentWidthMm}mm;
|
||||||
max-width: ${pageContentWidthMm}mm;
|
max-width: ${pageContentWidthMm}mm;
|
||||||
|
min-height: 100vh;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
background: #fff !important;
|
background: ${pageBackground} !important;
|
||||||
color: #16181d !important;
|
color: ${textColor} !important;
|
||||||
|
print-color-adjust: exact !important;
|
||||||
|
-webkit-print-color-adjust: exact !important;
|
||||||
}
|
}
|
||||||
.pdf-document { width: ${pageContentWidthMm}mm; box-sizing: border-box; }
|
.pdf-document {
|
||||||
.pdf-document-title { margin: 0 0 1.2em; padding: 0 0 .45em; border-bottom: 1px solid #d7dce2; }
|
width: ${pageContentWidthMm}mm;
|
||||||
.pdf-document-title h1 { margin: 0; font: 700 1.7rem/1.18 Inter, ui-sans-serif, system-ui, sans-serif; color: #111318; }
|
min-height: ${pageContentHeightMm}mm;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: ${pageBackground} !important;
|
||||||
|
color: ${textColor} !important;
|
||||||
|
}
|
||||||
|
.pdf-document-title { margin: 0 0 1.2em; padding: 0 0 .45em; border-bottom: 1px solid ${borderColor}; }
|
||||||
|
.pdf-document-title h1 { margin: 0; font: 700 1.7rem/1.18 Inter, ui-sans-serif, system-ui, sans-serif; color: ${headingColor}; }
|
||||||
.pdf-footer-spacer {
|
.pdf-footer-spacer {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -409,8 +497,8 @@ function printStyles(cssSize, pageWidthMm, pageHeightMm, marginMm, preserveColor
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin-top: 8mm;
|
margin-top: 8mm;
|
||||||
padding-top: 1.5mm;
|
padding-top: 1.5mm;
|
||||||
border-top: .2mm solid #e1e4e8;
|
border-top: .2mm solid ${borderColor};
|
||||||
color: #9299a3;
|
color: ${footerColor};
|
||||||
font: 500 7.5pt/1.2 Inter, ui-sans-serif, system-ui, sans-serif;
|
font: 500 7.5pt/1.2 Inter, ui-sans-serif, system-ui, sans-serif;
|
||||||
letter-spacing: .01em;
|
letter-spacing: .01em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -423,7 +511,7 @@ function printStyles(cssSize, pageWidthMm, pageHeightMm, marginMm, preserveColor
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
color: #16181d !important;
|
color: ${textColor} !important;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.pdf-content .preview-source-line::before { display: none !important; content: none !important; }
|
.pdf-content .preview-source-line::before { display: none !important; content: none !important; }
|
||||||
@@ -471,6 +559,10 @@ function printStyles(cssSize, pageWidthMm, pageHeightMm, marginMm, preserveColor
|
|||||||
.pdf-content p,
|
.pdf-content p,
|
||||||
.pdf-content li,
|
.pdf-content li,
|
||||||
.pdf-content dd { orphans: 3; widows: 3; }
|
.pdf-content dd { orphans: 3; widows: 3; }
|
||||||
|
.pdf-content p {
|
||||||
|
break-inside: avoid-page;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
.pdf-content ul,
|
.pdf-content ul,
|
||||||
.pdf-content ol,
|
.pdf-content ol,
|
||||||
@@ -545,6 +637,16 @@ function printStyles(cssSize, pageWidthMm, pageHeightMm, marginMm, preserveColor
|
|||||||
}
|
}
|
||||||
.pdf-content .pdf-keep-unit > :first-child { margin-top: 0 !important; }
|
.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-keep-unit > :last-child { margin-bottom: 0 !important; }
|
||||||
|
.pdf-content .pdf-keep-short {
|
||||||
|
break-inside: avoid-page !important;
|
||||||
|
page-break-inside: avoid !important;
|
||||||
|
}
|
||||||
|
.pdf-content .pdf-heading-lead > h1,
|
||||||
|
.pdf-content .pdf-heading-lead > h2,
|
||||||
|
.pdf-content .pdf-heading-lead > h3 {
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin-bottom: .18em;
|
||||||
|
}
|
||||||
.pdf-content .pdf-atomic-visual {
|
.pdf-content .pdf-atomic-visual {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -563,6 +665,24 @@ function printStyles(cssSize, pageWidthMm, pageHeightMm, marginMm, preserveColor
|
|||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
break-inside: avoid-page;
|
break-inside: avoid-page;
|
||||||
page-break-inside: avoid;
|
page-break-inside: avoid;
|
||||||
|
/* Dark PDF changes the document theme only. Never recolor raster images. */
|
||||||
|
filter: none !important;
|
||||||
|
-webkit-filter: none !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
mix-blend-mode: normal !important;
|
||||||
|
forced-color-adjust: none !important;
|
||||||
|
color-scheme: normal !important;
|
||||||
|
}
|
||||||
|
.pdf-content picture,
|
||||||
|
.pdf-content picture > img,
|
||||||
|
.pdf-content video,
|
||||||
|
.pdf-content canvas {
|
||||||
|
filter: none !important;
|
||||||
|
-webkit-filter: none !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
mix-blend-mode: normal !important;
|
||||||
|
forced-color-adjust: none !important;
|
||||||
|
color-scheme: normal !important;
|
||||||
}
|
}
|
||||||
.pdf-content .markdown-alias-image--sized {
|
.pdf-content .markdown-alias-image--sized {
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
@@ -601,9 +721,50 @@ function printStyles(cssSize, pageWidthMm, pageHeightMm, marginMm, preserveColor
|
|||||||
max-height: ${atomicMaxHeightMm}mm !important;
|
max-height: ${atomicMaxHeightMm}mm !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
${preserveColors ? `
|
|
||||||
.pdf-document,
|
.pdf-document,
|
||||||
.pdf-document * { print-color-adjust: exact !important; -webkit-print-color-adjust: exact !important; }
|
.pdf-document * { print-color-adjust: exact !important; -webkit-print-color-adjust: exact !important; }
|
||||||
|
|
||||||
|
${darkMode ? `
|
||||||
|
.pdf-content a { color: #b9b4d8 !important; }
|
||||||
|
.pdf-content code:not(pre code) { background: #242931 !important; border-color: #414852 !important; color: #e2e5e9 !important; }
|
||||||
|
.pdf-content pre { background: #1b1f25 !important; border-color: #3a414b !important; color: #d2d6dc !important; }
|
||||||
|
.pdf-content pre code,
|
||||||
|
.pdf-content .hljs { background: transparent !important; color: #d2d6dc !important; }
|
||||||
|
.pdf-content .hljs-keyword,
|
||||||
|
.pdf-content .hljs-selector-tag,
|
||||||
|
.pdf-content .hljs-literal,
|
||||||
|
.pdf-content .hljs-section,
|
||||||
|
.pdf-content .hljs-link { color: #c2acd0 !important; }
|
||||||
|
.pdf-content .hljs-string,
|
||||||
|
.pdf-content .hljs-attr,
|
||||||
|
.pdf-content .hljs-regexp,
|
||||||
|
.pdf-content .hljs-template-tag,
|
||||||
|
.pdf-content .hljs-template-variable { color: #b8c9a1 !important; }
|
||||||
|
.pdf-content .hljs-number,
|
||||||
|
.pdf-content .hljs-symbol,
|
||||||
|
.pdf-content .hljs-bullet { color: #d8a08e !important; }
|
||||||
|
.pdf-content .hljs-title,
|
||||||
|
.pdf-content .hljs-title.class_,
|
||||||
|
.pdf-content .hljs-title.function_,
|
||||||
|
.pdf-content .hljs-built_in,
|
||||||
|
.pdf-content .hljs-type { color: #9fb6d8 !important; }
|
||||||
|
.pdf-content .hljs-variable,
|
||||||
|
.pdf-content .hljs-selector-id,
|
||||||
|
.pdf-content .hljs-selector-class,
|
||||||
|
.pdf-content .hljs-property,
|
||||||
|
.pdf-content .hljs-attribute { color: #d39ca2 !important; }
|
||||||
|
.pdf-content .hljs-comment,
|
||||||
|
.pdf-content .hljs-quote { color: #9299a3 !important; font-style: italic; }
|
||||||
|
.pdf-content .hljs-meta { color: #cdbb8f !important; }
|
||||||
|
.pdf-content blockquote { border-left-color: #85818f !important; color: #cfd3d8 !important; }
|
||||||
|
.pdf-content mark { background: #5b5338 !important; color: #eee5b8 !important; }
|
||||||
|
.pdf-content th { background: #242930 !important; color: #e1e4e8 !important; }
|
||||||
|
.pdf-content .markdown-alert--success { border-color: #63806f !important; background: rgba(99, 128, 111, .12) !important; }
|
||||||
|
.pdf-content .markdown-alert--info { border-color: #6e8198 !important; background: rgba(110, 129, 152, .12) !important; }
|
||||||
|
.pdf-content .markdown-alert--warning { border-color: #9a8968 !important; background: rgba(154, 137, 104, .12) !important; }
|
||||||
|
.pdf-content .markdown-alert--danger { border-color: #956f73 !important; background: rgba(149, 111, 115, .12) !important; }
|
||||||
|
.pdf-content .task-checkbox { accent-color: #8c8798 !important; }
|
||||||
|
` : `
|
||||||
.pdf-content a { color: #5d50a5 !important; }
|
.pdf-content a { color: #5d50a5 !important; }
|
||||||
.pdf-content code:not(pre code) { background: #eeeaf7 !important; border-color: #bcb2a6 !important; color: #403a33 !important; }
|
.pdf-content code:not(pre code) { background: #eeeaf7 !important; border-color: #bcb2a6 !important; color: #403a33 !important; }
|
||||||
.pdf-content pre { background: #efeae2 !important; border-color: #e2dcd3 !important; color: #413b34 !important; }
|
.pdf-content pre { background: #efeae2 !important; border-color: #e2dcd3 !important; color: #413b34 !important; }
|
||||||
@@ -643,7 +804,7 @@ function printStyles(cssSize, pageWidthMm, pageHeightMm, marginMm, preserveColor
|
|||||||
.pdf-content .markdown-alert--warning { border-color: #a27831 !important; background: rgba(155, 112, 35, .11) !important; }
|
.pdf-content .markdown-alert--warning { border-color: #a27831 !important; background: rgba(155, 112, 35, .11) !important; }
|
||||||
.pdf-content .markdown-alert--danger { border-color: #a54b55 !important; background: rgba(155, 52, 67, .09) !important; }
|
.pdf-content .markdown-alert--danger { border-color: #a54b55 !important; background: rgba(155, 52, 67, .09) !important; }
|
||||||
.pdf-content .task-checkbox { accent-color: #6859b5 !important; }
|
.pdf-content .task-checkbox { accent-color: #6859b5 !important; }
|
||||||
` : ""}
|
`}
|
||||||
|
|
||||||
.pdf-page-break {
|
.pdf-page-break {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -658,9 +819,9 @@ function printStyles(cssSize, pageWidthMm, pageHeightMm, marginMm, preserveColor
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openPrintWindow({ preview, title, formatKey, marginMm, includeTitle, preserveColors }) {
|
function openPrintWindow({ preview, title, formatKey, marginMm, includeTitle, darkMode }) {
|
||||||
const format = PAGE_FORMATS[formatKey] || PAGE_FORMATS["a4-portrait"];
|
const format = PAGE_FORMATS[formatKey] || PAGE_FORMATS["a4-portrait"];
|
||||||
const safeMargin = [8, 12, 20].includes(Number(marginMm)) ? Number(marginMm) : 12;
|
const safeMargin = [0, 8, 12, 20].includes(Number(marginMm)) ? Number(marginMm) : 12;
|
||||||
const printWindow = window.open("", "_blank");
|
const printWindow = window.open("", "_blank");
|
||||||
if (!printWindow) throw new Error(t("editor.pdfPopupBlocked", {}, "The PDF window was blocked by the browser."));
|
if (!printWindow) throw new Error(t("editor.pdfPopupBlocked", {}, "The PDF window was blocked by the browser."));
|
||||||
try { printWindow.opener = null; } catch { /* same-origin hardening is best effort */ }
|
try { printWindow.opener = null; } catch { /* same-origin hardening is best effort */ }
|
||||||
@@ -671,15 +832,15 @@ function openPrintWindow({ preview, title, formatKey, marginMm, includeTitle, pr
|
|||||||
: "";
|
: "";
|
||||||
const lang = escapeHtml(document.documentElement.lang || "en");
|
const lang = escapeHtml(document.documentElement.lang || "en");
|
||||||
const base = escapeHtml(`${location.origin}/`);
|
const base = escapeHtml(`${location.origin}/`);
|
||||||
const css = printStyles(format.cssSize, format.widthMm, format.heightMm, safeMargin, preserveColors);
|
const css = printStyles(format.cssSize, format.widthMm, format.heightMm, safeMargin, darkMode);
|
||||||
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);
|
await ensurePrintableMermaid(content, darkMode);
|
||||||
if (preserveColors) await ensureSyntaxHighlight(content);
|
await ensureSyntaxHighlight(content);
|
||||||
|
|
||||||
printWindow.document.open();
|
printWindow.document.open();
|
||||||
printWindow.document.write(`<!doctype html><html lang="${lang}" data-theme="light"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><base href="${base}"><title>${escapeHtml(title || "RustPad")}</title>${stylesheetLinks()}<style>${css}</style></head><body><main class="pdf-document">${titleBlock}${content.outerHTML}<div class="pdf-footer-spacer" aria-hidden="true"></div><footer class="pdf-generated-footer" aria-hidden="true">${footerText}</footer></main></body></html>`);
|
printWindow.document.write(`<!doctype html><html lang="${lang}" data-theme="${darkMode ? "dark" : "light"}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><base href="${base}"><title>${escapeHtml(title || "RustPad")}</title>${stylesheetLinks()}<style>${css}</style></head><body><main class="pdf-document">${titleBlock}${content.outerHTML}<div class="pdf-footer-spacer" aria-hidden="true"></div><footer class="pdf-generated-footer" aria-hidden="true">${footerText}</footer></main></body></html>`);
|
||||||
printWindow.document.close();
|
printWindow.document.close();
|
||||||
|
|
||||||
const runPrint = async () => {
|
const runPrint = async () => {
|
||||||
@@ -707,15 +868,18 @@ export function bindPdfExport({ button, dialog, form, preview, title }) {
|
|||||||
if (!button || !dialog || !form || !preview) return;
|
if (!button || !dialog || !form || !preview) return;
|
||||||
const formatSelect = form.querySelector("#pdf-page-format");
|
const formatSelect = form.querySelector("#pdf-page-format");
|
||||||
const marginSelect = form.querySelector("#pdf-page-margin");
|
const marginSelect = form.querySelector("#pdf-page-margin");
|
||||||
|
const darkMode = form.querySelector("#pdf-dark-mode");
|
||||||
const includeTitle = form.querySelector("#pdf-include-title");
|
const includeTitle = form.querySelector("#pdf-include-title");
|
||||||
const preserveColors = form.querySelector("#pdf-preserve-colors");
|
|
||||||
const cancelButtons = form.querySelectorAll("[data-pdf-cancel]");
|
const cancelButtons = form.querySelectorAll("[data-pdf-cancel]");
|
||||||
|
|
||||||
const savedFormat = saved("format", "a4-portrait");
|
const savedFormat = saved("format", "a4-portrait");
|
||||||
formatSelect.value = Object.hasOwn(PAGE_FORMATS, savedFormat) ? savedFormat : "a4-portrait";
|
formatSelect.value = Object.hasOwn(PAGE_FORMATS, savedFormat) ? savedFormat : "a4-portrait";
|
||||||
marginSelect.value = ["8", "12", "20"].includes(saved("margin", "12")) ? saved("margin", "12") : "12";
|
if (marginSelect) {
|
||||||
|
const savedMargin = saved("margin", "12");
|
||||||
|
marginSelect.value = ["0", "8", "12", "20"].includes(savedMargin) ? savedMargin : "12";
|
||||||
|
}
|
||||||
|
if (darkMode) darkMode.checked = saved("dark-mode", "on") !== "off";
|
||||||
includeTitle.checked = saved("include-title", "off") === "on";
|
includeTitle.checked = saved("include-title", "off") === "on";
|
||||||
if (preserveColors) preserveColors.checked = saved("preserve-colors", "on") !== "off";
|
|
||||||
|
|
||||||
button.addEventListener("click", () => dialog.showModal());
|
button.addEventListener("click", () => dialog.showModal());
|
||||||
cancelButtons.forEach(cancel => cancel.addEventListener("click", () => dialog.close("cancel")));
|
cancelButtons.forEach(cancel => cancel.addEventListener("click", () => dialog.close("cancel")));
|
||||||
@@ -724,11 +888,11 @@ export function bindPdfExport({ button, dialog, form, preview, title }) {
|
|||||||
form.addEventListener("submit", event => {
|
form.addEventListener("submit", event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const formatKey = formatSelect.value;
|
const formatKey = formatSelect.value;
|
||||||
const marginMm = Number(marginSelect.value);
|
const marginMm = Number(marginSelect?.value ?? 12);
|
||||||
save("format", formatKey);
|
save("format", formatKey);
|
||||||
save("margin", marginMm);
|
save("margin", marginMm);
|
||||||
|
save("dark-mode", darkMode?.checked === false ? "off" : "on");
|
||||||
save("include-title", includeTitle.checked ? "on" : "off");
|
save("include-title", includeTitle.checked ? "on" : "off");
|
||||||
save("preserve-colors", preserveColors?.checked === false ? "off" : "on");
|
|
||||||
try {
|
try {
|
||||||
const printJob = openPrintWindow({
|
const printJob = openPrintWindow({
|
||||||
preview,
|
preview,
|
||||||
@@ -736,7 +900,7 @@ export function bindPdfExport({ button, dialog, form, preview, title }) {
|
|||||||
formatKey,
|
formatKey,
|
||||||
marginMm,
|
marginMm,
|
||||||
includeTitle: includeTitle.checked,
|
includeTitle: includeTitle.checked,
|
||||||
preserveColors: preserveColors?.checked !== false,
|
darkMode: darkMode?.checked !== false,
|
||||||
});
|
});
|
||||||
dialog.close("confirm");
|
dialog.close("confirm");
|
||||||
void printJob.catch(error => {
|
void printJob.catch(error => {
|
||||||
|
|||||||
Reference in new issue
Block a user