Skip to content
13 changes: 12 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
</script>
<link rel="stylesheet" type="text/css" href="css/style.css?v=1.11.0">
<link rel="icon" type="image/png" href="favicon.png">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<script type="module" src="/src/main.js?v=1.11.0"></script>
<title>Markdown Live Preview</title>
</head>
Expand All @@ -58,7 +59,17 @@
<span style="margin-left:12px;"><input type="checkbox" id="theme-checkbox"><label for="theme-checkbox">Dark mode</label></span>
</div>
</div>
<div id="github"><a href="https://github.com/tanabe/markdown-live-preview"><img src="image/GitHub-Mark-Light-32px.webp"></a></div>
<div id="right-menu-items">
<div id="download-menu">
<a href="#" id="download-button"><i class="fas fa-download"></i></a>
<div class="dropdown-content">
<a href="#" id="export-html">Export as HTML</a>
<a href="#" id="export-pdf">Export as PDF</a>
<a href="#" id="export-doc">Export as DOC</a>
</div>
</div>
<div id="github"><a href="https://github.com/tanabe/markdown-live-preview"><img src="image/GitHub-Mark-Light-32px.webp"></a></div>
</div>
</header>

<div id="container" class="split-container">
Expand Down
119 changes: 91 additions & 28 deletions public/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
pre, blockquote, table, img {
page-break-inside: avoid;
}
}
151 changes: 151 additions & 0 deletions src/export.js
Original file line number Diff line number Diff line change
@@ -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 = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${escapeHtml(title)}</title>
<link rel="stylesheet" href="${cssCdn}" />
<style>
body { margin: 0; padding: 24px; }
.markdown-body { box-sizing: border-box; max-width: 980px; margin: 0 auto; }
</style>
</head>
<body>
<article class="markdown-body">${bodyHtml}</article>
</body>
</html>`;
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 = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${escapeHtml(title)}</title>
<link rel="stylesheet" href="${cssCdn}" />
<link rel="stylesheet" href="${printCss}" media="print" />
<style>
/* Additional print-specific styles if needed, or overrides */
@page {
margin: 10mm;
}
html, body {
padding: 10mm !important;
overflow: hidden !important;
}
article.markdown-body {
padding: 0;
}
table {
border-collapse: collapse;
width: 100%;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f2f2f2;
}
</style>
</head>
<body>
<article class="markdown-body">${bodyHtml}</article>
</body>
</html>`;

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, '"')
.replace(/'/g, '&#039;');
}
Loading