From 6067c0a505ef6f975942bf2000dfd885a677cd1e Mon Sep 17 00:00:00 2001 From: Arindam Sahoo Date: Tue, 4 Nov 2025 12:52:15 +0530 Subject: [PATCH 1/7] feat(export): Add PDF and DOC download functionality Introduces a new download menu in the header, allowing users to export the rendered Markdown content. This commit: - Adds a "Download" button with a dropdown menu for export options. - Integrates jsPDF and html2canvas libraries to enable PDF generation from the live preview content. - Includes Font Awesome for the download icon. - Adds a placeholder link for future DOC download functionality. - Updates package dependencies to include html2canvas and its sub-dependencies. --- index.html | 13 ++++++- package-lock.json | 45 +++++++++++++++++++++++ package.json | 1 + public/css/style.css | 55 +++++++++++++++++++++++++++- src/main.js | 86 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 198 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index afbee53..474d660 100644 --- a/index.html +++ b/index.html @@ -9,6 +9,8 @@ + + Markdown Live Preview @@ -32,7 +34,16 @@
Copy
-
+
+ +
+
diff --git a/package-lock.json b/package-lock.json index fedab72..5bc5547 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "dompurify": "^3.2.5", "github-markdown-css": "^5.8.1", + "html2canvas": "^1.4.1", "marked": "^15.0.7", "monaco-editor": "^0.52.2", "storehouse-js": "github:tanabe/Storehouse-js" @@ -762,6 +763,14 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -853,6 +862,14 @@ "node": ">= 0.4.0" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -1163,6 +1180,18 @@ "node": ">=12" } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/http-proxy": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", @@ -1570,6 +1599,14 @@ "node": ">=8" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -1606,6 +1643,14 @@ "dev": true, "license": "MIT" }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/vite": { "version": "6.3.6", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", diff --git a/package.json b/package.json index bf4ca6f..a6b7d1f 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "dependencies": { "dompurify": "^3.2.5", "github-markdown-css": "^5.8.1", + "html2canvas": "^1.4.1", "marked": "^15.0.7", "monaco-editor": "^0.52.2", "storehouse-js": "github:tanabe/Storehouse-js" diff --git a/public/css/style.css b/public/css/style.css index 6187a31..fd0517c 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -84,8 +84,53 @@ header input[type="checkbox"] { margin-top: 0px; } +#download-menu { + position: relative; + display: inline-block; + margin-left: 16px; +} + +#download-button { + color: #fff; + font-size: 16px; /* Adjust size as needed */ + padding: 8px; + cursor: pointer; +} + +.dropdown-content { + display: none; + position: absolute; + background-color: #f9f9f9; + min-width: 160px; + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + z-index: 1; + right: 0; /* Align dropdown to the right of the button */ +} + +.dropdown-content a { + color: black; + padding: 12px 16px; + text-decoration: none; + display: block; + text-align: left; +} + +.dropdown-content a:hover { + background-color: #f1f1f1; +} + +#download-menu:hover .dropdown-content { + display: block; +} + +#right-menu-items { + display: flex; + align-items: center; +} + #github { padding-right: 32px; + margin-left: 16px; /* Add some space between download and github icons */ } #github img { @@ -98,6 +143,14 @@ header input[type="checkbox"] { width: 16px; } +.dropdown-content a { + color: #fff; /* Match navbar color */ + padding: 12px 16px; + text-decoration: none; + display: block; + text-align: left; +} + footer { padding: 8px; bottom: 0; @@ -169,4 +222,4 @@ footer { .split-divider.active { background: #666; -} \ No newline at end of file +} diff --git a/src/main.js b/src/main.js index 2aa4626..c757299 100644 --- a/src/main.js +++ b/src/main.js @@ -2,6 +2,7 @@ import Storehouse from 'storehouse-js'; import * as monaco from 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/+esm'; import { marked } from 'marked'; import DOMPurify from 'dompurify'; +import html2canvas from 'html2canvas'; // Re-import html2canvas import 'github-markdown-css/github-markdown-light.css'; const init = () => { @@ -347,6 +348,91 @@ This web site is using ${"`"}markedjs/marked${"`"}. setupResetButton(); setupCopyButton(editor); + // ----- download menu ----- + let setupDownloadMenu = (editor) => { + const downloadButton = document.getElementById('download-button'); + const downloadMenu = document.getElementById('download-menu'); + const dropdownContent = downloadMenu.querySelector('.dropdown-content'); + + // Toggle dropdown visibility + downloadButton.addEventListener('click', (event) => { + event.preventDefault(); + dropdownContent.style.display = dropdownContent.style.display === 'block' ? 'none' : 'block'; + }); + + // Close the dropdown if the user clicks outside of it + window.addEventListener('click', (event) => { + if (!downloadMenu.contains(event.target)) { + dropdownContent.style.display = 'none'; + } + }); + + // Download PDF + document.getElementById('download-pdf').addEventListener('click', (event) => { + event.preventDefault(); + const { jsPDF } = window.jspdf; + const doc = new jsPDF(); + const outputElement = document.getElementById('output'); + doc.html(outputElement, { + callback: function (doc) { + doc.save('markdown-preview.pdf'); + }, + x: 10, + y: 10, + html2canvas: { + scale: 0.8 // Adjust scale for better fit + } + }); + doc.setFontSize(12); + const lineHeight = doc.getTextDimensions('T').h; // Estimate line height + + for (let i = 0; i < lines.length; i++) { + if (y + lineHeight > pageHeight - margin) { + doc.addPage(); + y = margin; + } + doc.text(lines[i], margin, y); + y += lineHeight; + } + + doc.save('markdown-preview.pdf'); + dropdownContent.style.display = 'none'; // Close dropdown after action + }); + + // Download DOC (as HTML saved with .doc extension) + document.getElementById('download-doc').addEventListener('click', (event) => { + event.preventDefault(); + const outputElement = document.getElementById('output'); + const content = outputElement.innerHTML; + const filename = 'markdown-preview.doc'; + const blob = new Blob(['Document' + content + ''], { + type: 'application/msword' + }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + dropdownContent.style.display = 'none'; // Close dropdown after action + }); + + // Copy Markdown to clipboard + document.getElementById('copy-markdown').addEventListener('click', (event) => { + event.preventDefault(); + let value = editor.getValue(); + copyToClipboard(value, () => { + alert('Markdown copied to clipboard!'); + }, + () => { + alert('Failed to copy markdown to clipboard.'); + }); + dropdownContent.style.display = 'none'; // Close dropdown after action + }); + }; + + setupDownloadMenu(editor); + let scrollBarSettings = loadScrollBarSettings() || false; initScrollBarSync(scrollBarSettings); From c28d52842616468903ccf29aac460c93fe054de7 Mon Sep 17 00:00:00 2001 From: Arindam Sahoo Date: Tue, 4 Nov 2025 12:55:04 +0530 Subject: [PATCH 2/7] chore(comments): removed Comments because I already adjusted them --- public/css/style.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/css/style.css b/public/css/style.css index fd0517c..356592c 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -92,7 +92,7 @@ header input[type="checkbox"] { #download-button { color: #fff; - font-size: 16px; /* Adjust size as needed */ + font-size: 16px; padding: 8px; cursor: pointer; } @@ -130,7 +130,7 @@ header input[type="checkbox"] { #github { padding-right: 32px; - margin-left: 16px; /* Add some space between download and github icons */ + margin-left: 16px; } #github img { From 10e775c63d12473e39e1a47f3f1f963d1c3466c9 Mon Sep 17 00:00:00 2001 From: Arindam Sahoo Date: Tue, 4 Nov 2025 13:01:20 +0530 Subject: [PATCH 3/7] style: Improve readability of dropdown menu links Updated the text color for links within dropdown menus from white (`#fff`) to dark gray (`#333`). This change enhances readability, especially when the dropdown background is light. The addition of `!important` ensures the new color is applied consistently, overriding any conflicting styles. --- public/css/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/css/style.css b/public/css/style.css index 356592c..7a6e39b 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -144,7 +144,7 @@ header input[type="checkbox"] { } .dropdown-content a { - color: #fff; /* Match navbar color */ + color: #333 !important; padding: 12px 16px; text-decoration: none; display: block; From ab258927c19d8a6f6bfd7cc245d17820349ddeae Mon Sep 17 00:00:00 2001 From: Arindam Sahoo Date: Tue, 4 Nov 2025 17:21:11 +0530 Subject: [PATCH 4/7] feat: Add HTML export and refactor document export functionality Refactor the document export mechanism, introducing an "Export as HTML" option and renaming existing "Download" options to "Export". This commit removes the `html2canvas` dependency and its related sub-dependencies, streamlining the project's build. The direct CDN link for `jspdf` was also removed from `index.html`, implying a shift to a bundled version or an alternative PDF generation method. Cache busting versions in `index.html` were updated to reflect these changes. --- index.html | 10 ++-- package-lock.json | 45 ----------------- package.json | 3 +- public/css/print.css | 43 ++++++++++++++++ src/export.js | 115 +++++++++++++++++++++++++++++++++++++++++++ src/main.js | 53 ++++++-------------- 6 files changed, 178 insertions(+), 91 deletions(-) create mode 100644 public/css/print.css create mode 100644 src/export.js diff --git a/index.html b/index.html index 474d660..5b6bc63 100644 --- a/index.html +++ b/index.html @@ -7,11 +7,10 @@ - + - - + Markdown Live Preview @@ -38,8 +37,9 @@
diff --git a/package-lock.json b/package-lock.json index 5bc5547..fedab72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "dependencies": { "dompurify": "^3.2.5", "github-markdown-css": "^5.8.1", - "html2canvas": "^1.4.1", "marked": "^15.0.7", "monaco-editor": "^0.52.2", "storehouse-js": "github:tanabe/Storehouse-js" @@ -763,14 +762,6 @@ "dev": true, "license": "MIT" }, - "node_modules/base64-arraybuffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", - "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -862,14 +853,6 @@ "node": ">= 0.4.0" } }, - "node_modules/css-line-break": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", - "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", - "dependencies": { - "utrie": "^1.0.2" - } - }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -1180,18 +1163,6 @@ "node": ">=12" } }, - "node_modules/html2canvas": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", - "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", - "dependencies": { - "css-line-break": "^2.1.0", - "text-segmentation": "^1.0.3" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/http-proxy": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", @@ -1599,14 +1570,6 @@ "node": ">=8" } }, - "node_modules/text-segmentation": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", - "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", - "dependencies": { - "utrie": "^1.0.2" - } - }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -1643,14 +1606,6 @@ "dev": true, "license": "MIT" }, - "node_modules/utrie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", - "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", - "dependencies": { - "base64-arraybuffer": "^1.0.2" - } - }, "node_modules/vite": { "version": "6.3.6", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", diff --git a/package.json b/package.json index a6b7d1f..1309691 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,7 @@ }, "dependencies": { "dompurify": "^3.2.5", - "github-markdown-css": "^5.8.1", - "html2canvas": "^1.4.1", + "github-markdown-css": "^5.8.1", "marked": "^15.0.7", "monaco-editor": "^0.52.2", "storehouse-js": "github:tanabe/Storehouse-js" diff --git a/public/css/print.css b/public/css/print.css new file mode 100644 index 0000000..5cc55ac --- /dev/null +++ b/public/css/print.css @@ -0,0 +1,43 @@ +/* Print styles for PDF export via browser print */ + +@page { + margin: 20mm; +} + +/* Reset general styles for print */ +* { + -webkit-print-color-adjust: exact; + print-color-adjust: exact; +} + +html, body { + background: #fff !important; + color: #000 !important; +} + +/* Hide UI chrome */ +header, footer, #split-divider, #editor, #editor-wrapper, #menu-items, #github { + display: none !important; +} + +/* Ensure preview content is visible and fits page */ +#preview, #preview-wrapper, .markdown-body { + display: block !important; + width: 100% !important; + max-width: 100% !important; + overflow: visible !important; +} + +/* Content wrapper on print window */ +article.markdown-body { + box-sizing: border-box; +} + +/* Page break rules to improve pagination */ +h1, h2, h3 { + page-break-after: avoid; +} + +pre, blockquote, table, img { + page-break-inside: avoid; +} diff --git a/src/export.js b/src/export.js new file mode 100644 index 0000000..ace14ea --- /dev/null +++ b/src/export.js @@ -0,0 +1,115 @@ +// Export utilities for HTML, PDF (via browser print), and DOCX +// Keeps everything client-side and uses the already-sanitized preview HTML + +// Load a non-ESM script once and return a promise when its global is available +function loadScriptOnce(url, globalVar) { + return new Promise((resolve, reject) => { + if (globalVar && typeof window !== 'undefined' && window[globalVar]) { + resolve(window[globalVar]); + return; + } + const existing = document.querySelector(`script[data-dynamic="${url}"]`); + if (existing) { + existing.addEventListener('load', () => resolve(window[globalVar])); + existing.addEventListener('error', reject); + return; + } + const script = document.createElement('script'); + script.src = url; + script.async = true; + script.defer = true; + script.setAttribute('data-dynamic', url); + script.onload = () => resolve(window[globalVar]); + script.onerror = reject; + document.head.appendChild(script); + }); +} + +export function getDocumentTitleFromMarkdown(markdown) { + try { + const m = markdown.match(/^#\s+(.+)$/m); + let title = (m && m[1]) ? m[1].trim() : 'document'; + // sanitize filename + title = title.replace(/[\\/:*?"<>|\n\r\t]/g, ' ').trim(); + if (!title) title = 'document'; + return title; + } catch (_) { + return 'document'; + } +} + +export function buildExportHTML({ bodyHtml, title }) { + // Wrap sanitized HTML in a standalone document + // Use GitHub Markdown CSS via CDN for portability + const cssCdn = 'https://cdn.jsdelivr.net/npm/github-markdown-css@5.8.1/github-markdown-light.min.css'; + const doc = ` + + + + + ${escapeHtml(title)} + + + + +
${bodyHtml}
+ +`; + return doc; +} + +export function downloadBlob({ blob, filename }) { + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); +} + +export function exportAsHTML({ bodyHtml, title }) { + const html = buildExportHTML({ bodyHtml, title }); + const blob = new Blob([html], { type: 'text/html;charset=utf-8' }); + downloadBlob({ blob, filename: `${title}.html` }); +} + +export function exportAsPDF({ bodyHtml, title }) { + // Open a print-ready window using same origin to allow resource loading + const cssCdn = 'https://cdn.jsdelivr.net/npm/github-markdown-css@5.8.1/github-markdown-light.min.css'; + const printCss = '/css/print.css'; + const win = window.open('', '_blank'); + if (!win) { + alert('Popup blocked. Please allow popups to export as PDF.'); + return; + } + win.document.open(); + win.document.write(` + + + + + ${escapeHtml(title)} + + + + +
${bodyHtml}
+ + +`); + win.document.close(); +} + +function escapeHtml(str) { + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} diff --git a/src/main.js b/src/main.js index c757299..abf3732 100644 --- a/src/main.js +++ b/src/main.js @@ -2,8 +2,8 @@ import Storehouse from 'storehouse-js'; import * as monaco from 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/+esm'; import { marked } from 'marked'; import DOMPurify from 'dompurify'; -import html2canvas from 'html2canvas'; // Re-import html2canvas import 'github-markdown-css/github-markdown-light.css'; +import { getDocumentTitleFromMarkdown, exportAsHTML, exportAsPDF } from './export.js'; const init = () => { let hasEdited = false; @@ -368,39 +368,17 @@ This web site is using ${"`"}markedjs/marked${"`"}. }); // Download PDF - document.getElementById('download-pdf').addEventListener('click', (event) => { + document.getElementById('export-pdf').addEventListener('click', (event) => { event.preventDefault(); - const { jsPDF } = window.jspdf; - const doc = new jsPDF(); const outputElement = document.getElementById('output'); - doc.html(outputElement, { - callback: function (doc) { - doc.save('markdown-preview.pdf'); - }, - x: 10, - y: 10, - html2canvas: { - scale: 0.8 // Adjust scale for better fit - } - }); - doc.setFontSize(12); - const lineHeight = doc.getTextDimensions('T').h; // Estimate line height - - for (let i = 0; i < lines.length; i++) { - if (y + lineHeight > pageHeight - margin) { - doc.addPage(); - y = margin; - } - doc.text(lines[i], margin, y); - y += lineHeight; - } - - doc.save('markdown-preview.pdf'); + const content = outputElement.innerHTML; + const title = getDocumentTitleFromMarkdown(editor.getValue()); + exportAsPDF({ bodyHtml: content, title: title }); dropdownContent.style.display = 'none'; // Close dropdown after action }); - // Download DOC (as HTML saved with .doc extension) - document.getElementById('download-doc').addEventListener('click', (event) => { + // Download DOC + document.getElementById('export-doc').addEventListener('click', (event) => { event.preventDefault(); const outputElement = document.getElementById('output'); const content = outputElement.innerHTML; @@ -414,19 +392,16 @@ This web site is using ${"`"}markedjs/marked${"`"}. document.body.appendChild(link); link.click(); document.body.removeChild(link); - dropdownContent.style.display = 'none'; // Close dropdown after action + dropdownContent.style.display = 'none'; }); - // Copy Markdown to clipboard - document.getElementById('copy-markdown').addEventListener('click', (event) => { + // Download HTML + document.getElementById('export-html').addEventListener('click', (event) => { event.preventDefault(); - let value = editor.getValue(); - copyToClipboard(value, () => { - alert('Markdown copied to clipboard!'); - }, - () => { - alert('Failed to copy markdown to clipboard.'); - }); + const outputElement = document.getElementById('output'); + const content = outputElement.innerHTML; + const title = getDocumentTitleFromMarkdown(editor.getValue()); + exportAsHTML({ bodyHtml: content, title: title }); dropdownContent.style.display = 'none'; // Close dropdown after action }); }; From 822531a9b7addf760fd4438529a7f5ba5afe5473 Mon Sep 17 00:00:00 2001 From: Arindam Sahoo Date: Tue, 4 Nov 2025 17:22:20 +0530 Subject: [PATCH 5/7] reverting involuntary space --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1309691..bf4ca6f 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ }, "dependencies": { "dompurify": "^3.2.5", - "github-markdown-css": "^5.8.1", + "github-markdown-css": "^5.8.1", "marked": "^15.0.7", "monaco-editor": "^0.52.2", "storehouse-js": "github:tanabe/Storehouse-js" From d0cf4f15f7d016e311bf0da64d2f8b55e5bbd48e Mon Sep 17 00:00:00 2001 From: Arindam Sahoo Date: Tue, 4 Nov 2025 17:35:54 +0530 Subject: [PATCH 6/7] feat(print): Improve PDF export and browser printing styles This commit introduces dedicated print styles to enhance the user experience when printing content directly from the browser or exporting to PDF. Changes include: * Added a comprehensive `@media print` block to `public/css/style.css`. These styles ensure that only the markdown content is visible, UI elements are hidden, and the content is properly paginated and formatted for print. * Updated `src/export.js` to load `public/css/style.css` with `media="print"` instead of a separate `print.css` file. This consolidates print-specific styles into the main stylesheet, simplifying maintenance. These changes provide a cleaner, more readable output for printed documents and PDF exports. --- index.html | 4 ++-- public/css/print.css | 43 ----------------------------------------- public/css/style.css | 46 ++++++++++++++++++++++++++++++++++++++++++++ src/export.js | 3 +-- 4 files changed, 49 insertions(+), 47 deletions(-) delete mode 100644 public/css/print.css diff --git a/index.html b/index.html index 5b6bc63..c82acd8 100644 --- a/index.html +++ b/index.html @@ -7,10 +7,10 @@ - + - + Markdown Live Preview diff --git a/public/css/print.css b/public/css/print.css deleted file mode 100644 index 5cc55ac..0000000 --- a/public/css/print.css +++ /dev/null @@ -1,43 +0,0 @@ -/* Print styles for PDF export via browser print */ - -@page { - margin: 20mm; -} - -/* Reset general styles for print */ -* { - -webkit-print-color-adjust: exact; - print-color-adjust: exact; -} - -html, body { - background: #fff !important; - color: #000 !important; -} - -/* Hide UI chrome */ -header, footer, #split-divider, #editor, #editor-wrapper, #menu-items, #github { - display: none !important; -} - -/* Ensure preview content is visible and fits page */ -#preview, #preview-wrapper, .markdown-body { - display: block !important; - width: 100% !important; - max-width: 100% !important; - overflow: visible !important; -} - -/* Content wrapper on print window */ -article.markdown-body { - box-sizing: border-box; -} - -/* Page break rules to improve pagination */ -h1, h2, h3 { - page-break-after: avoid; -} - -pre, blockquote, table, img { - page-break-inside: avoid; -} diff --git a/public/css/style.css b/public/css/style.css index 7a6e39b..1323ebb 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -223,3 +223,49 @@ footer { .split-divider.active { background: #666; } + +@media print { + /* Print styles for PDF export via browser print */ + + @page { + margin: 20mm; + } + + /* Reset general styles for print */ + * { + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } + + html, body { + background: #fff !important; + color: #000 !important; + } + + /* Hide UI chrome */ + header, footer, #split-divider, #editor, #editor-wrapper, #menu-items, #github { + display: none !important; + } + + /* Ensure preview content is visible and fits page */ + #preview, #preview-wrapper, .markdown-body { + display: block !important; + width: 100% !important; + max-width: 100% !important; + overflow: visible !important; + } + + /* Content wrapper on print window */ + article.markdown-body { + box-sizing: border-box; + } + + /* Page break rules to improve pagination */ + h1, h2, h3 { + page-break-after: avoid; + } + + pre, blockquote, table, img { + page-break-inside: avoid; + } +} diff --git a/src/export.js b/src/export.js index ace14ea..1bf725e 100644 --- a/src/export.js +++ b/src/export.js @@ -81,7 +81,6 @@ export function exportAsHTML({ bodyHtml, title }) { export function exportAsPDF({ bodyHtml, title }) { // Open a print-ready window using same origin to allow resource loading const cssCdn = 'https://cdn.jsdelivr.net/npm/github-markdown-css@5.8.1/github-markdown-light.min.css'; - const printCss = '/css/print.css'; const win = window.open('', '_blank'); if (!win) { alert('Popup blocked. Please allow popups to export as PDF.'); @@ -95,7 +94,7 @@ export function exportAsPDF({ bodyHtml, title }) { ${escapeHtml(title)} - +
${bodyHtml}
From a5f2ad853c65e93acd136425eca7b725a433cc57 Mon Sep 17 00:00:00 2001 From: Arindam Sahoo Date: Tue, 4 Nov 2025 18:13:40 +0530 Subject: [PATCH 7/7] feat: Improve PDF export with Blob URLs and enhanced print styles Refactor the `exportAsPDF` function to generate PDF content using Blob URLs. This change improves the reliability and security of the export process by avoiding direct `document.write` operations in the new print window. Additionally, this commit introduces specific print media styles directly into the generated HTML. These styles ensure consistent page margins, proper content padding, and basic table formatting, leading to a more professional and readable PDF output. The print window now automatically closes after printing, and Blob URLs are revoked for resource cleanup. --- src/export.js | 61 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/src/export.js b/src/export.js index 1bf725e..27d621a 100644 --- a/src/export.js +++ b/src/export.js @@ -79,29 +79,66 @@ export function exportAsHTML({ bodyHtml, title }) { } export function exportAsPDF({ bodyHtml, title }) { - // Open a print-ready window using same origin to allow resource loading const cssCdn = 'https://cdn.jsdelivr.net/npm/github-markdown-css@5.8.1/github-markdown-light.min.css'; - const win = window.open('', '_blank'); - if (!win) { - alert('Popup blocked. Please allow popups to export as PDF.'); - return; - } - win.document.open(); - win.document.write(` + const printCss = '/css/style.css'; // Reference the main stylesheet for print media + + const htmlContent = ` ${escapeHtml(title)} - + +
${bodyHtml}
- -`); - win.document.close(); +`; + + const blob = new Blob([htmlContent], { type: 'text/html;charset=utf-8' }); + const url = URL.createObjectURL(blob); + + const newWindow = window.open(url, '_blank'); + if (!newWindow) { + alert('Popup blocked. Please allow popups to export as PDF.'); + URL.revokeObjectURL(url); + return; + } + + newWindow.addEventListener('load', () => { + newWindow.print(); + URL.revokeObjectURL(url); // Clean up the Blob URL after printing + }); + + newWindow.addEventListener('afterprint', () => { + newWindow.close(); // Close the window after printing + }); } function escapeHtml(str) {