removed client-side export/added modern latex notation
This commit is contained in:
parent
cd9b5e7d7b
commit
96b5fd8cfc
|
|
@ -348,8 +348,8 @@ def generate_export_html(
|
|||
<script>
|
||||
MathJax = {{
|
||||
tex: {{
|
||||
inlineMath: [['$', '$']],
|
||||
displayMath: [['$$', '$$']],
|
||||
inlineMath: [['\\\\(', '\\\\)'], ['$', '$']],
|
||||
displayMath: [['\\\\[', '\\\\]'], ['$$', '$$']],
|
||||
processEscapes: true,
|
||||
processEnvironments: true
|
||||
}},
|
||||
|
|
@ -674,6 +674,21 @@ def generate_export_html(
|
|||
<div class="markdown-preview" id="content"></div>
|
||||
|
||||
<script>
|
||||
// Protect LaTeX delimiters \\(...\\) and \\[...\\] from marked.js escaping
|
||||
marked.use({{
|
||||
extensions: [{{
|
||||
name: 'protectLatexMath',
|
||||
level: 'inline',
|
||||
start(src) {{ return src.match(/\\\\[\\(\\[]/)?.index; }},
|
||||
tokenizer(src) {{
|
||||
const match = src.match(/^(\\\\[\\(\\[])([\\s\\S]*?)(\\\\[\\)\\]])/);
|
||||
if (match) {{
|
||||
return {{ type: 'html', raw: match[0], text: match[0] }};
|
||||
}}
|
||||
}}
|
||||
}}]
|
||||
}});
|
||||
|
||||
// Configure marked
|
||||
marked.setOptions({{
|
||||
gfm: true,
|
||||
|
|
@ -681,7 +696,7 @@ def generate_export_html(
|
|||
headerIds: true,
|
||||
mangle: false
|
||||
}});
|
||||
|
||||
|
||||
// Raw markdown content
|
||||
const markdown = `{escaped_content}`;
|
||||
|
||||
|
|
@ -691,6 +706,11 @@ def generate_export_html(
|
|||
const safeHtml = DOMPurify.sanitize(rawHtml);
|
||||
document.getElementById('content').innerHTML = safeHtml;
|
||||
|
||||
// Typeset math after content is inserted
|
||||
if (typeof MathJax !== 'undefined' && MathJax.typeset) {{
|
||||
MathJax.typeset();
|
||||
}}
|
||||
|
||||
// Add copy buttons to code blocks
|
||||
document.querySelectorAll('.markdown-preview pre').forEach(pre => {{
|
||||
const btn = document.createElement('button');
|
||||
|
|
|
|||
|
|
@ -1228,6 +1228,89 @@ async def remove_note(request: Request, note_path: str):
|
|||
raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to delete note"))
|
||||
|
||||
|
||||
@api_router.get("/export/{note_path:path}", tags=["Export"])
|
||||
@limiter.limit("30/minute")
|
||||
async def export_note_to_html(request: Request, note_path: str, theme: Optional[str] = None):
|
||||
"""
|
||||
Export a note as a standalone HTML file.
|
||||
|
||||
The HTML includes all necessary CSS, MathJax, Mermaid, and syntax highlighting
|
||||
for offline viewing. Images are embedded as base64.
|
||||
|
||||
Query Parameters:
|
||||
theme: Optional theme name (defaults to current theme or 'light')
|
||||
|
||||
Returns:
|
||||
HTML file download
|
||||
"""
|
||||
try:
|
||||
notes_dir = Path(config['storage']['notes_dir'])
|
||||
|
||||
# Read note content
|
||||
content = get_note_content(str(notes_dir), note_path)
|
||||
if content is None:
|
||||
raise HTTPException(status_code=404, detail="Note not found")
|
||||
|
||||
# Run on_note_load hook (can transform content, e.g., decrypt)
|
||||
transformed_content = plugin_manager.run_hook('on_note_load', note_path=note_path, content=content)
|
||||
if transformed_content is not None:
|
||||
content = transformed_content
|
||||
|
||||
# Strip YAML frontmatter (like the preview does)
|
||||
content = strip_frontmatter(content)
|
||||
|
||||
# Get note folder for resolving relative image paths
|
||||
note_file_path = notes_dir / note_path
|
||||
note_folder = note_file_path.parent
|
||||
|
||||
# Embed images as base64
|
||||
content_with_images = embed_images_as_base64(content, note_folder, notes_dir)
|
||||
|
||||
# Convert wikilinks to decorative HTML links
|
||||
content_with_links = convert_wikilinks_to_html(content_with_images)
|
||||
|
||||
# Get theme CSS
|
||||
themes_dir = Path(__file__).parent.parent / "themes"
|
||||
theme_name = theme or 'light'
|
||||
theme_css = get_theme_css(str(themes_dir), theme_name)
|
||||
if not theme_css:
|
||||
theme_css = get_theme_css(str(themes_dir), "light")
|
||||
theme_name = "light"
|
||||
|
||||
# Strip data-theme selector
|
||||
theme_css = theme_css.replace(f':root[data-theme="{theme_name}"]', ':root')
|
||||
theme_css = theme_css.replace(':root[data-theme="light"]', ':root')
|
||||
theme_css = theme_css.replace(':root[data-theme="dark"]', ':root')
|
||||
|
||||
# Determine if dark theme
|
||||
is_dark = 'dark' in theme_name.lower() or theme_name in ['dracula', 'nord', 'monokai', 'cobalt2', 'gruvbox-dark']
|
||||
|
||||
# Get note title
|
||||
title = Path(note_path).stem
|
||||
|
||||
# Generate HTML
|
||||
html_content = generate_export_html(
|
||||
title=title,
|
||||
content=content_with_links,
|
||||
theme_css=theme_css,
|
||||
is_dark=is_dark
|
||||
)
|
||||
|
||||
# Return as downloadable file
|
||||
filename = f"{title}.html"
|
||||
return Response(
|
||||
content=html_content,
|
||||
media_type="text/html",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="{filename}"'
|
||||
}
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to export note"))
|
||||
|
||||
|
||||
@api_router.get("/search", tags=["Search"])
|
||||
async def search(
|
||||
q: str,
|
||||
|
|
|
|||
|
|
@ -444,6 +444,49 @@ Returns the relationship graph between notes with link detection.
|
|||
- **Markdown links** - `[text](note.md)` standard internal links
|
||||
- **Edge types** - `"wikilink"` or `"markdown"` to distinguish link source
|
||||
|
||||
---
|
||||
|
||||
## 📤 Export
|
||||
|
||||
### Export Note as HTML
|
||||
```http
|
||||
GET /api/export/{note_path}?theme={theme_name}
|
||||
```
|
||||
|
||||
Exports a note as a standalone HTML file with all dependencies embedded for offline viewing.
|
||||
|
||||
**Parameters:**
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `note_path` | path | Path to the note (e.g., `folder/note.md`) |
|
||||
| `theme` | query (optional) | Theme name for styling (defaults to `light`) |
|
||||
|
||||
**Response:**
|
||||
Returns an HTML file download with `Content-Disposition: attachment` header.
|
||||
|
||||
**Features:**
|
||||
- Fully self-contained HTML with embedded CSS
|
||||
- Images converted to base64 data URLs
|
||||
- MathJax for LaTeX math rendering (supports `$...$`, `$$...$$`, `\(...\)`, `\[...\]`)
|
||||
- Mermaid.js for diagram rendering
|
||||
- Highlight.js for syntax highlighting
|
||||
- Wikilinks converted to decorative spans
|
||||
- YAML frontmatter stripped
|
||||
- Responsive design with print support
|
||||
|
||||
**Rate Limit:** 30 requests/minute
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
# Export with default theme
|
||||
curl -O http://localhost:8000/api/export/notes/Welcome.md
|
||||
|
||||
# Export with dark theme
|
||||
curl -O "http://localhost:8000/api/export/docs/API.md?theme=dracula"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ System
|
||||
|
||||
### Get Config
|
||||
|
|
|
|||
|
|
@ -4,14 +4,18 @@ NoteDiscovery supports **LaTeX mathematical notation** powered by MathJax 3. Wri
|
|||
|
||||
## Syntax Overview
|
||||
|
||||
### Inline Math (within text)
|
||||
Use `$...$` for inline equations:
|
||||
| Delimiter | Type | Behavior |
|
||||
|-----------|------|----------|
|
||||
| `$...$` | Inline | Flows with text, not centered |
|
||||
| `\(...\)` | Inline | Same as `$...$` (LaTeX standard) |
|
||||
| `$$...$$` | Display | Own paragraph, centered, larger |
|
||||
| `\[...\]` | Display | Same as `$$...$$` (LaTeX standard) |
|
||||
|
||||
- `$E = mc^2$` renders as: $E = mc^2$
|
||||
- `$x^2 + y^2 = r^2$` renders as: $x^2 + y^2 = r^2$
|
||||
### Inline Math (within text)
|
||||
Inline math flows with your text: `$E = mc^2$` renders as $E = mc^2$
|
||||
|
||||
### Display Math (centered, on its own line)
|
||||
Use `$$...$$` for display equations:
|
||||
Display math gets its own centered paragraph:
|
||||
|
||||
```markdown
|
||||
$$
|
||||
|
|
@ -19,6 +23,13 @@ x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}
|
|||
$$
|
||||
```
|
||||
|
||||
Or using LaTeX-style delimiters:
|
||||
```markdown
|
||||
\[
|
||||
x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}
|
||||
\]
|
||||
```
|
||||
|
||||
$$
|
||||
x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}
|
||||
$$
|
||||
|
|
|
|||
303
frontend/app.js
303
frontend/app.js
|
|
@ -4164,6 +4164,26 @@ function noteApp() {
|
|||
return codeBlocks[parseInt(index)];
|
||||
});
|
||||
|
||||
// Protect LaTeX \(...\) and \[...\] delimiters from marked.js escaping
|
||||
marked.use({
|
||||
extensions: [{
|
||||
name: 'protectLatexMath',
|
||||
level: 'inline',
|
||||
start(src) { return src.match(/\\[\(\[]/)?.index; },
|
||||
tokenizer(src) {
|
||||
// Match \(...\) or \[...\]
|
||||
const match = src.match(/^(\\[\(\[])([\s\S]*?)(\\[\)\]])/);
|
||||
if (match) {
|
||||
return {
|
||||
type: 'html',
|
||||
raw: match[0],
|
||||
text: match[0]
|
||||
};
|
||||
}
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
// Configure marked with syntax highlighting
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
|
|
@ -5045,7 +5065,7 @@ function noteApp() {
|
|||
}, CONFIG.SCROLL_SYNC_DELAY);
|
||||
},
|
||||
|
||||
// Export current note as HTML
|
||||
// Export current note as HTML via backend API
|
||||
async exportToHTML() {
|
||||
if (!this.currentNote || !this.noteContent) {
|
||||
alert(this.t('notes.no_content'));
|
||||
|
|
@ -5053,278 +5073,39 @@ function noteApp() {
|
|||
}
|
||||
|
||||
try {
|
||||
// Get the note name without extension
|
||||
const noteName = this.currentNoteName || 'note';
|
||||
|
||||
// Get current rendered HTML (this already has markdown converted and will have LaTeX delimiters)
|
||||
let renderedHTML = this.renderedMarkdown;
|
||||
|
||||
// Convert non-image media (audio, video, PDF) to placeholders first
|
||||
// These shouldn't be embedded as base64 (too large)
|
||||
// Use CSS variables with fallbacks for theme-aware styling (matches backend export.py)
|
||||
const mediaPlaceholder = (type, name) => {
|
||||
const icons = { audio: '🎵', video: '🎬', document: '📄' };
|
||||
const labels = { audio: 'Audio file', video: 'Video file', document: 'PDF document' };
|
||||
const icon = icons[type] || '📎';
|
||||
const label = labels[type] || 'Media file';
|
||||
return `<div style="margin:1.5rem 0;padding:1.5rem;background:linear-gradient(135deg,var(--bg-tertiary,#f8f9fa) 0%,var(--bg-secondary,#e9ecef) 100%);border:1px solid var(--border-primary,#dee2e6);border-radius:0.5rem;display:flex;align-items:center;gap:1rem;">
|
||||
<span style="font-size:2rem;">${icon}</span>
|
||||
<div>
|
||||
<div style="font-weight:600;color:var(--text-primary,#212529);">${name}</div>
|
||||
<div style="font-size:0.875rem;color:var(--text-secondary,#6c757d);">${label} — not available in exported view</div>
|
||||
</div>
|
||||
</div>`;
|
||||
};
|
||||
|
||||
// Replace audio embeds with placeholders
|
||||
renderedHTML = renderedHTML.replace(
|
||||
/<div class="media-embed media-audio">.*?<audio[^>]*src="[^"]*\/([^"\/]+)"[^>]*>.*?<\/div>/gs,
|
||||
(match, filename) => mediaPlaceholder('audio', decodeURIComponent(filename).replace(/\.[^.]+$/, ''))
|
||||
);
|
||||
|
||||
// Replace video embeds with placeholders
|
||||
renderedHTML = renderedHTML.replace(
|
||||
/<div class="media-embed media-video">.*?<video[^>]*src="[^"]*\/([^"\/]+)"[^>]*>.*?<\/div>/gs,
|
||||
(match, filename) => mediaPlaceholder('video', decodeURIComponent(filename).replace(/\.[^.]+$/, ''))
|
||||
);
|
||||
|
||||
// Replace PDF embeds with placeholders
|
||||
renderedHTML = renderedHTML.replace(
|
||||
/<div class="media-embed media-pdf">.*?<iframe[^>]*src="[^"]*\/([^"\/]+)"[^>]*>.*?<\/div>/gs,
|
||||
(match, filename) => mediaPlaceholder('document', decodeURIComponent(filename).replace(/\.[^.]+$/, ''))
|
||||
);
|
||||
|
||||
// Embed local images as base64 for fully self-contained HTML
|
||||
// Handle both /api/media/ and legacy /api/images/ paths
|
||||
const imgRegex = /src="\/api\/(?:media|images)\/([^"]+)"/g;
|
||||
const imgMatches = [...renderedHTML.matchAll(imgRegex)];
|
||||
|
||||
for (const match of imgMatches) {
|
||||
const encodedPath = match[1];
|
||||
// Skip non-image files (already handled above)
|
||||
const ext = encodedPath.split('.').pop().toLowerCase();
|
||||
if (!['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch the image
|
||||
const imgResponse = await fetch(`/api/media/${encodedPath}`);
|
||||
if (imgResponse.ok) {
|
||||
const blob = await imgResponse.blob();
|
||||
// Convert to base64 data URL
|
||||
const base64 = await new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
// Replace the src with base64 data URL
|
||||
renderedHTML = renderedHTML.replace(match[0], `src="${base64}"`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Failed to embed image: ${encodedPath}`, e);
|
||||
// Fall back to relative path
|
||||
const decodedPath = decodeURIComponent(encodedPath);
|
||||
renderedHTML = renderedHTML.replace(match[0], `src="${decodedPath}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// Get current theme CSS
|
||||
// Build API URL with current theme
|
||||
const currentTheme = this.currentTheme || 'light';
|
||||
const themeResponse = await fetch(`/api/themes/${currentTheme}`);
|
||||
const themeText = await themeResponse.text();
|
||||
const encodedPath = this.currentNote.split('/').map(s => encodeURIComponent(s)).join('/');
|
||||
const url = `/api/export/${encodedPath}?theme=${encodeURIComponent(currentTheme)}`;
|
||||
|
||||
// Check if response is JSON or plain CSS
|
||||
let themeCss;
|
||||
try {
|
||||
const themeJson = JSON.parse(themeText);
|
||||
// If it's JSON, extract the css field
|
||||
themeCss = themeJson.css || themeText;
|
||||
} catch (e) {
|
||||
// If it's not JSON, use it as-is
|
||||
themeCss = themeText;
|
||||
// Fetch the exported HTML from backend
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: 'Export failed' }));
|
||||
throw new Error(error.detail || 'Export failed');
|
||||
}
|
||||
|
||||
// Theme CSS uses :root[data-theme="..."] selector, but we need plain :root for export
|
||||
// Strip the data-theme attribute selector so variables apply globally
|
||||
themeCss = themeCss.replace(/:root\[data-theme="[^"]+"\]/g, ':root');
|
||||
|
||||
// Get highlight.js theme URL from current page
|
||||
const highlightLinkElement = document.getElementById('highlight-theme');
|
||||
if (!highlightLinkElement || !highlightLinkElement.href) {
|
||||
console.warn('Could not detect highlight.js theme, export may not match preview exactly');
|
||||
}
|
||||
const highlightTheme = highlightLinkElement ? highlightLinkElement.href : '';
|
||||
|
||||
// Extract all markdown preview styles from current page
|
||||
let markdownStyles = '';
|
||||
const styleSheets = Array.from(document.styleSheets);
|
||||
|
||||
for (const sheet of styleSheets) {
|
||||
try {
|
||||
// Skip external stylesheets (CDN resources) to avoid CORS errors
|
||||
// We link them directly in the exported HTML anyway
|
||||
if (sheet.href && (sheet.href.startsWith('http://') || sheet.href.startsWith('https://'))) {
|
||||
const currentOrigin = window.location.origin;
|
||||
const sheetURL = new URL(sheet.href);
|
||||
if (sheetURL.origin !== currentOrigin) {
|
||||
// Skip cross-origin stylesheets (they're linked directly in export)
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const rules = Array.from(sheet.cssRules || []);
|
||||
for (const rule of rules) {
|
||||
const cssText = rule.cssText;
|
||||
// Include rules that target markdown-preview, mjx-container, or mermaid-rendered
|
||||
if (cssText.includes('.markdown-preview') ||
|
||||
cssText.includes('mjx-container') ||
|
||||
cssText.includes('.MathJax') ||
|
||||
cssText.includes('.mermaid-rendered')) {
|
||||
markdownStyles += cssText + '\n';
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Gracefully skip stylesheets that can't be accessed
|
||||
// (This should rarely happen now that we skip external stylesheets)
|
||||
console.debug('Skipping stylesheet:', sheet.href);
|
||||
// Get filename from Content-Disposition header or use note name
|
||||
let filename = (this.currentNoteName || 'note') + '.html';
|
||||
const contentDisposition = response.headers.get('Content-Disposition');
|
||||
if (contentDisposition) {
|
||||
const match = contentDisposition.match(/filename="([^"]+)"/);
|
||||
if (match) {
|
||||
filename = match[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Create standalone HTML document with MathJax
|
||||
const htmlDocument = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${noteName}</title>
|
||||
|
||||
<!-- Highlight.js for code syntax highlighting (v11.9.0) -->
|
||||
${highlightTheme ? `<link rel="stylesheet" href="${highlightTheme}">` : '<!-- No highlight.js theme detected -->'}
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
|
||||
<!-- MathJax for LaTeX math rendering (v3.2.2) -->
|
||||
<script>
|
||||
MathJax = {
|
||||
tex: {
|
||||
inlineMath: [['$', '$']],
|
||||
displayMath: [['$$', '$$']],
|
||||
processEscapes: true,
|
||||
processEnvironments: true
|
||||
},
|
||||
options: {
|
||||
skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code']
|
||||
},
|
||||
startup: {
|
||||
pageReady: () => {
|
||||
return MathJax.startup.defaultPageReady().then(() => {
|
||||
// Highlight code blocks after MathJax is done (exclude diagram renderers)
|
||||
document.querySelectorAll('pre code:not(.language-mermaid)').forEach((block) => {
|
||||
hljs.highlightElement(block);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/mathjax@3.2.2/es5/tex-mml-chtml.js"></script>
|
||||
|
||||
<!-- Mermaid.js for diagrams -->
|
||||
<script type="module">
|
||||
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11.12.2/dist/mermaid.esm.min.mjs';
|
||||
const isDark = ${this.getThemeType() === 'dark'};
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: isDark ? 'dark' : 'default',
|
||||
securityLevel: 'strict',
|
||||
fontFamily: 'inherit',
|
||||
flowchart: { useMaxWidth: true },
|
||||
sequence: { useMaxWidth: true },
|
||||
gantt: { useMaxWidth: true },
|
||||
state: { useMaxWidth: true },
|
||||
er: { useMaxWidth: true },
|
||||
pie: { useMaxWidth: true },
|
||||
mindmap: { useMaxWidth: true },
|
||||
gitGraph: { useMaxWidth: true }
|
||||
});
|
||||
|
||||
// Render any Mermaid code blocks
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const mermaidBlocks = document.querySelectorAll('pre code.language-mermaid');
|
||||
for (let i = 0; i < mermaidBlocks.length; i++) {
|
||||
const block = mermaidBlocks[i];
|
||||
const pre = block.parentElement;
|
||||
try {
|
||||
const code = block.textContent;
|
||||
const id = 'mermaid-diagram-' + i;
|
||||
const { svg } = await mermaid.render(id, code);
|
||||
const container = document.createElement('div');
|
||||
container.className = 'mermaid-rendered';
|
||||
container.style.cssText = 'background-color: transparent; padding: 20px; text-align: center; overflow-x: auto;';
|
||||
container.innerHTML = svg;
|
||||
pre.parentElement.replaceChild(container, pre);
|
||||
} catch (error) {
|
||||
console.error('Mermaid rendering error:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Theme CSS */
|
||||
${themeCss}
|
||||
|
||||
/* Base styles */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 2rem;
|
||||
max-width: 900px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Markdown preview styles extracted from current page */
|
||||
${markdownStyles}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
padding: 0.5in;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="markdown-preview">
|
||||
${renderedHTML}
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
// Create blob and download
|
||||
const blob = new Blob([htmlDocument], { type: 'text/html;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
// Download as blob
|
||||
const blob = await response.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${noteName}.html`;
|
||||
a.href = blobUrl;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
// Cleanup
|
||||
URL.revokeObjectURL(url);
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
document.body.removeChild(a);
|
||||
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -70,8 +70,8 @@
|
|||
<script>
|
||||
MathJax = {
|
||||
tex: {
|
||||
inlineMath: [['$', '$']],
|
||||
displayMath: [['$$', '$$']],
|
||||
inlineMath: [['\\(', '\\)'], ['$', '$']],
|
||||
displayMath: [['\\[', '\\]'], ['$$', '$$']],
|
||||
processEscapes: true,
|
||||
processEnvironments: true
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue