Skip to content

Commit 0c1f3ed

Browse files
authored
Merge pull request #10 from gamosoft/features/export-html
added html export function
2 parents 4cb7396 + 28be46a commit 0c1f3ed

File tree

4 files changed

+177
-0
lines changed

4 files changed

+177
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ NoteDiscovery is a **lightweight, self-hosted note-taking application** that put
3939
- 📱 **Responsive** - Works on desktop, tablet, and mobile
4040
- 📂 **Simple Storage** - Plain markdown files in folders
4141
- 🧮 **Math Support** - LaTeX/MathJax for beautiful equations
42+
- 📄 **HTML Export** - Share notes as standalone HTML files
4243

4344
## 🚀 Quick Start
4445

data/notes/FEATURES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
- **Syntax highlighting** for code blocks (50+ languages)
1111
- **Copy code blocks** - One-click copy button on hover
1212
- **LaTeX/Math rendering** - Beautiful mathematical equations with MathJax
13+
- **HTML Export** - Export notes as standalone HTML files
1314

1415
### Organization
1516
- **Folder hierarchy** - Organize notes in nested folders

frontend/app.js

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2125,6 +2125,170 @@ function noteApp() {
21252125
setTimeout(() => {
21262126
this.isScrolling = false;
21272127
}, CONFIG.SCROLL_SYNC_DELAY);
2128+
},
2129+
2130+
// Export current note as HTML
2131+
async exportToHTML() {
2132+
if (!this.currentNote || !this.noteContent) {
2133+
alert('No note content to export');
2134+
return;
2135+
}
2136+
2137+
try {
2138+
// Get the note name without extension
2139+
const noteName = this.currentNoteName || 'note';
2140+
2141+
// Get current rendered HTML (this already has markdown converted and will have LaTeX delimiters)
2142+
const renderedHTML = this.renderedMarkdown;
2143+
2144+
// Get current theme CSS
2145+
const currentTheme = this.currentTheme || 'light';
2146+
const themeResponse = await fetch(`/api/themes/${currentTheme}`);
2147+
const themeText = await themeResponse.text();
2148+
2149+
// Check if response is JSON or plain CSS
2150+
let themeCss;
2151+
try {
2152+
const themeJson = JSON.parse(themeText);
2153+
// If it's JSON, extract the css field
2154+
themeCss = themeJson.css || themeText;
2155+
} catch (e) {
2156+
// If it's not JSON, use it as-is
2157+
themeCss = themeText;
2158+
}
2159+
2160+
// Theme CSS uses :root[data-theme="..."] selector, but we need plain :root for export
2161+
// Strip the data-theme attribute selector so variables apply globally
2162+
themeCss = themeCss.replace(/:root\[data-theme="[^"]+"\]/g, ':root');
2163+
2164+
// Get highlight.js theme URL from current page
2165+
const highlightLinkElement = document.getElementById('highlight-theme');
2166+
if (!highlightLinkElement || !highlightLinkElement.href) {
2167+
console.warn('Could not detect highlight.js theme, export may not match preview exactly');
2168+
}
2169+
const highlightTheme = highlightLinkElement ? highlightLinkElement.href : '';
2170+
2171+
// Extract all markdown preview styles from current page
2172+
let markdownStyles = '';
2173+
const styleSheets = Array.from(document.styleSheets);
2174+
2175+
for (const sheet of styleSheets) {
2176+
try {
2177+
const rules = Array.from(sheet.cssRules || []);
2178+
for (const rule of rules) {
2179+
const cssText = rule.cssText;
2180+
// Include rules that target markdown-preview or mjx-container
2181+
if (cssText.includes('.markdown-preview') ||
2182+
cssText.includes('mjx-container') ||
2183+
cssText.includes('.MathJax')) {
2184+
markdownStyles += cssText + '\n';
2185+
}
2186+
}
2187+
} catch (e) {
2188+
// Skip stylesheets that can't be accessed (CORS)
2189+
console.warn('Could not access stylesheet:', sheet.href, e);
2190+
}
2191+
}
2192+
2193+
// Create standalone HTML document with MathJax
2194+
const htmlDocument = `<!DOCTYPE html>
2195+
<html lang="en">
2196+
<head>
2197+
<meta charset="UTF-8">
2198+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
2199+
<title>${noteName}</title>
2200+
2201+
<!-- Highlight.js for code syntax highlighting -->
2202+
${highlightTheme ? `<link rel="stylesheet" href="${highlightTheme}">` : '<!-- No highlight.js theme detected -->'}
2203+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
2204+
2205+
<!-- MathJax for LaTeX math rendering (same config as preview) -->
2206+
<script>
2207+
MathJax = {
2208+
tex: {
2209+
inlineMath: [['$', '$']],
2210+
displayMath: [['$$', '$$']],
2211+
processEscapes: true,
2212+
processEnvironments: true
2213+
},
2214+
options: {
2215+
skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code']
2216+
},
2217+
startup: {
2218+
pageReady: () => {
2219+
return MathJax.startup.defaultPageReady().then(() => {
2220+
// Highlight code blocks after MathJax is done
2221+
document.querySelectorAll('pre code').forEach((block) => {
2222+
hljs.highlightElement(block);
2223+
});
2224+
});
2225+
}
2226+
}
2227+
};
2228+
</script>
2229+
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
2230+
2231+
<style>
2232+
/* Theme CSS */
2233+
${themeCss}
2234+
2235+
/* Base styles */
2236+
* {
2237+
box-sizing: border-box;
2238+
}
2239+
2240+
body {
2241+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
2242+
margin: 0;
2243+
padding: 2rem;
2244+
max-width: 900px;
2245+
margin-left: auto;
2246+
margin-right: auto;
2247+
background-color: var(--bg-primary);
2248+
color: var(--text-primary);
2249+
}
2250+
2251+
/* Markdown preview styles extracted from current page */
2252+
${markdownStyles}
2253+
2254+
@media (max-width: 768px) {
2255+
body {
2256+
padding: 1rem;
2257+
}
2258+
}
2259+
2260+
@media print {
2261+
body {
2262+
padding: 0.5in;
2263+
max-width: none;
2264+
}
2265+
}
2266+
</style>
2267+
</head>
2268+
<body>
2269+
<div class="markdown-preview">
2270+
${renderedHTML}
2271+
</div>
2272+
</body>
2273+
</html>`;
2274+
2275+
// Create blob and download
2276+
const blob = new Blob([htmlDocument], { type: 'text/html;charset=utf-8' });
2277+
const url = URL.createObjectURL(blob);
2278+
const a = document.createElement('a');
2279+
a.href = url;
2280+
a.download = `${noteName}.html`;
2281+
document.body.appendChild(a);
2282+
a.click();
2283+
2284+
// Cleanup
2285+
URL.revokeObjectURL(url);
2286+
document.body.removeChild(a);
2287+
2288+
} catch (error) {
2289+
console.error('HTML export failed:', error);
2290+
alert(`Failed to export HTML: ${error.message}`);
2291+
}
21282292
}
21292293
}
21302294
}

frontend/index.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -920,6 +920,17 @@ <h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary);" x-text="
920920
Preview
921921
</button>
922922
</div>
923+
924+
<!-- Export HTML Button -->
925+
<button
926+
x-show="currentNote"
927+
@click="exportToHTML()"
928+
class="px-3 py-1.5 text-sm rounded transition hover:opacity-80"
929+
style="background-color: var(--accent-secondary); color: var(--text-primary);"
930+
title="Export as HTML"
931+
>
932+
📄 Export HTML
933+
</button>
923934
</div>
924935
</div>
925936

0 commit comments

Comments
 (0)