diff --git a/index.html b/index.html
index 6f6b1a0..76fecf1 100644
--- a/index.html
+++ b/index.html
@@ -32,6 +32,7 @@
+
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);