@@ -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 ( / : r o o t \[ d a t a - t h e m e = " [ ^ " ] + " \] / 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}
0 commit comments