Files
markdown-renderer/templates/index.html
2026-03-01 11:35:14 -06:00

755 lines
25 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Markdown Editor with LaTeX</title>
<!-- MathJax for LaTeX rendering -->
<script>
MathJax = {
tex: {
inlineMath: [['$', '$'], ['\\(', '\\)']],
displayMath: [['$$', '$$'], ['\\[', '\\]']]
},
svg: {
fontCache: 'global'
}
};
</script>
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-svg.js"></script>
<!-- Highlight.js for code syntax highlighting -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
height: 100vh;
overflow: hidden;
}
.container {
display: flex;
height: 100vh;
}
.pane {
flex: 1;
overflow: auto;
padding: 20px;
}
.editor-pane {
background-color: #f5f5f5;
border-right: 2px solid #ddd;
}
.preview-pane {
background-color: #ffffff;
padding: 40px;
position: relative;
}
.export-buttons {
position: absolute;
top: 10px;
right: 10px;
display: flex;
gap: 10px;
z-index: 1000;
flex-wrap: wrap;
}
.export-button {
padding: 10px 20px;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: background-color 0.3s;
}
.pdf-button {
background-color: #4CAF50;
}
.pdf-button:hover {
background-color: #45a049;
}
.pdf-button:active {
background-color: #3d8b40;
}
.latex-button {
background-color: #2196F3;
}
.latex-button:hover {
background-color: #0b7dda;
}
.latex-button:active {
background-color: #0968b8;
}
.clipboard-button {
background-color: #FF9800;
}
.clipboard-button:hover {
background-color: #F57C00;
}
.clipboard-button:active {
background-color: #E65100;
}
.export-button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
#editor {
width: 100%;
height: calc(100vh - 40px);
padding: 15px;
font-family: 'Monaco', 'Courier New', monospace;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
resize: none;
background-color: #ffffff;
line-height: 1.6;
}
#editor:focus {
outline: none;
border-color: #4CAF50;
}
#preview {
line-height: 1.8;
color: #333;
}
/* Markdown styling */
#preview h1 {
font-size: 2em;
margin: 0.67em 0;
border-bottom: 2px solid #eee;
padding-bottom: 0.3em;
}
#preview h2 {
font-size: 1.5em;
margin: 0.75em 0;
border-bottom: 1px solid #eee;
padding-bottom: 0.3em;
}
#preview h3 {
font-size: 1.25em;
margin: 0.83em 0;
}
#preview h4, #preview h5, #preview h6 {
margin: 1em 0;
}
#preview p {
margin: 1em 0;
}
#preview code {
background-color: #f5f5f5;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Monaco', 'Courier New', monospace;
font-size: 0.9em;
}
#preview pre {
background-color: #f6f8fa;
padding: 16px;
border-radius: 6px;
overflow-x: auto;
margin: 1em 0;
}
#preview pre code {
background-color: transparent;
padding: 0;
}
#preview blockquote {
border-left: 4px solid #ddd;
padding-left: 16px;
margin: 1em 0;
color: #666;
}
#preview ul, #preview ol {
margin: 1em 0;
padding-left: 2em;
}
#preview li {
margin: 0.5em 0;
}
#preview table {
border-collapse: collapse;
width: 100%;
margin: 1em 0;
}
#preview th, #preview td {
border: 1px solid #ddd;
padding: 8px 12px;
text-align: left;
}
#preview th {
background-color: #f5f5f5;
font-weight: bold;
}
#preview tr:nth-child(even) {
background-color: #f9f9f9;
}
#preview a {
color: #0366d6;
text-decoration: none;
}
#preview a:hover {
text-decoration: underline;
}
#preview img {
max-width: 100%;
height: auto;
}
#preview hr {
border: none;
border-top: 2px solid #eee;
margin: 2em 0;
}
/* LaTeX equation styling */
.MathJax {
font-size: 1.1em;
}
</style>
</head>
<body>
<div class="container">
<div class="pane editor-pane">
<textarea id="editor" placeholder="Type or paste your markdown here...
Examples:
# Heading 1
## Heading 2
**Bold text** and *italic text*
Inline equation: $E = mc^2$
Block equation:
$$
\int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi}
$$
- List item 1
- List item 2
```python
def hello():
print('Hello, World!')
```
"></textarea>
</div>
<div class="pane preview-pane">
<div class="export-buttons">
<button id="clipboardButton" class="export-button clipboard-button">Copy to Clipboard</button>
<button id="latexButton" class="export-button latex-button">Export to LaTeX</button>
<button id="pdfButton" class="export-button pdf-button">Export to PDF</button>
</div>
<div id="preview"></div>
</div>
</div>
<script>
const editor = document.getElementById('editor');
const preview = document.getElementById('preview');
let debounceTimer;
// Function to render markdown
async function renderMarkdown() {
const markdownText = editor.value;
try {
const response = await fetch('/render', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ markdown: markdownText })
});
const data = await response.json();
preview.innerHTML = data.html;
// Apply syntax highlighting to code blocks
preview.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
});
// Trigger MathJax to render LaTeX
if (window.MathJax) {
MathJax.typesetPromise([preview]).catch((err) => console.log(err));
}
} catch (error) {
console.error('Error rendering markdown:', error);
}
}
// Debounce input to avoid too many requests
editor.addEventListener('input', () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(renderMarkdown, 300);
});
// Handle Tab key in editor
editor.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
e.preventDefault();
// Get cursor position
const start = editor.selectionStart;
const end = editor.selectionEnd;
// Insert tab character
const value = editor.value;
editor.value = value.substring(0, start) + '\t' + value.substring(end);
// Move cursor after the tab
editor.selectionStart = editor.selectionEnd = start + 1;
// Trigger re-render
editor.dispatchEvent(new Event('input'));
}
});
// Helper function to convert SVG to PNG data URL
async function svgToPng(svgElement, width, height) {
return new Promise((resolve, reject) => {
// Create a canvas
const canvas = document.createElement('canvas');
const scale = 2; // 2x for better quality
canvas.width = width * scale;
canvas.height = height * scale;
const ctx = canvas.getContext('2d');
// Scale for better quality
ctx.scale(scale, scale);
// White background
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, width, height);
// Serialize SVG to string
const svgString = new XMLSerializer().serializeToString(svgElement);
const svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(svgBlob);
// Load SVG into an image
const img = new Image();
img.onload = () => {
ctx.drawImage(img, 0, 0, width, height);
URL.revokeObjectURL(url);
resolve(canvas.toDataURL('image/png'));
};
img.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error('Failed to load SVG'));
};
img.src = url;
});
}
// Copy to Clipboard function
const clipboardButton = document.getElementById('clipboardButton');
clipboardButton.addEventListener('click', async () => {
const button = clipboardButton;
const originalText = button.textContent;
button.disabled = true;
button.textContent = 'Copying...';
try {
// Wait for MathJax to finish rendering
if (window.MathJax) {
await MathJax.typesetPromise([preview]);
}
// Clone the preview to avoid modifying the displayed content
const previewClone = preview.cloneNode(true);
// Find the MathJax defs (font path definitions)
let mjxDefs = document.getElementById('MJX-SVG-global-cache');
if (!mjxDefs) {
const allDefs = document.querySelectorAll('svg defs');
if (allDefs.length > 0) {
mjxDefs = allDefs[allDefs.length - 1];
}
}
// Convert MathJax equations to PNG images
const mjxContainers = previewClone.querySelectorAll('mjx-container');
const originalContainers = preview.querySelectorAll('mjx-container');
for (let i = 0; i < mjxContainers.length; i++) {
const container = mjxContainers[i];
const originalContainer = originalContainers[i];
const svg = container.querySelector('svg');
const originalSvg = originalContainer.querySelector('svg');
if (svg && originalSvg) {
// Get dimensions from the original (rendered) SVG
const bbox = originalSvg.getBoundingClientRect();
const width = bbox.width;
const height = bbox.height;
// Set explicit dimensions on the SVG
svg.setAttribute('width', width + 'px');
svg.setAttribute('height', height + 'px');
// Ensure xmlns
if (!svg.hasAttribute('xmlns')) {
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
}
// Add MathJax font definitions if found and not already present
if (mjxDefs && !svg.querySelector('defs')) {
const defsClone = mjxDefs.cloneNode(true);
svg.insertBefore(defsClone, svg.firstChild);
}
// Convert SVG to PNG
const pngDataUrl = await svgToPng(svg, width, height);
// Create an img element
const img = document.createElement('img');
img.src = pngDataUrl;
img.style.verticalAlign = 'middle';
// Scale to 70%
img.width = width * 0.7;
img.height = height * 0.7;
// Check if it's a display equation
const isDisplay = container.hasAttribute('display') ||
window.getComputedStyle(originalContainer).display === 'block';
if (isDisplay) {
img.style.display = 'block';
img.style.margin = '1em auto';
} else {
img.style.display = 'inline-block';
}
// Replace the container with the image
container.replaceWith(img);
}
}
// Apply inline styles to tables for better Word compatibility
const tables = previewClone.querySelectorAll('table');
tables.forEach(table => {
table.style.borderCollapse = 'collapse';
table.style.width = '100%';
table.style.margin = '1em 0';
const cells = table.querySelectorAll('th, td');
cells.forEach(cell => {
cell.style.border = '1px solid #ddd';
cell.style.padding = '8px 12px';
cell.style.textAlign = 'left';
});
const headers = table.querySelectorAll('th');
headers.forEach(th => {
th.style.backgroundColor = '#f5f5f5';
th.style.fontWeight = 'bold';
});
const rows = table.querySelectorAll('tr');
rows.forEach((row, index) => {
if (index % 2 === 1) { // Even rows (0-indexed, so odd index)
row.style.backgroundColor = '#f9f9f9';
}
});
});
// Get the HTML content with styling
const styledHtml = `
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; line-height: 1.8; color: #333;">
${previewClone.innerHTML}
</div>
`;
// Create a blob with HTML content
const blob = new Blob([styledHtml], { type: 'text/html' });
// Use the Clipboard API to copy both HTML and plain text
await navigator.clipboard.write([
new ClipboardItem({
'text/html': blob,
'text/plain': new Blob([previewClone.innerText], { type: 'text/plain' })
})
]);
button.textContent = 'Copied!';
setTimeout(() => {
button.textContent = originalText;
}, 2000);
} catch (error) {
console.error('Error copying to clipboard:', error);
alert('Failed to copy to clipboard. Please try again.');
} finally {
button.disabled = false;
}
});
// LaTeX export function
const latexButton = document.getElementById('latexButton');
latexButton.addEventListener('click', async () => {
const button = latexButton;
const originalText = button.textContent;
button.disabled = true;
button.textContent = 'Generating LaTeX...';
try {
const markdownText = editor.value;
// Send to backend for LaTeX generation
const response = await fetch('/export-latex', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ markdown: markdownText })
});
if (!response.ok) {
throw new Error('LaTeX generation failed');
}
const data = await response.json();
const latexContent = data.latex;
// Download the LaTeX file
const blob = new Blob([latexContent], { type: 'text/plain' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'document.tex';
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
console.error('Error generating LaTeX:', error);
alert('Failed to generate LaTeX. Please try again.');
} finally {
button.disabled = false;
button.textContent = originalText;
}
});
// PDF export function
const pdfButton = document.getElementById('pdfButton');
pdfButton.addEventListener('click', async () => {
const button = pdfButton;
const originalText = button.textContent;
button.disabled = true;
button.textContent = 'Generating PDF...';
try {
// Wait for MathJax to finish rendering
if (window.MathJax) {
await MathJax.typesetPromise([preview]);
}
// First, collect dimensions from the original DOM elements (before cloning)
const dimensions = [];
const originalContainers = preview.querySelectorAll('mjx-container');
originalContainers.forEach(container => {
const svg = container.querySelector('svg');
if (svg) {
const bbox = svg.getBoundingClientRect();
dimensions.push({ width: bbox.width, height: bbox.height });
}
});
// Clone the preview to avoid modifying the displayed content
const previewClone = preview.cloneNode(true);
// Find the MathJax defs (font path definitions)
// MathJax SVG mode stores font paths in a global cache SVG element
let mjxDefs = document.getElementById('MJX-SVG-global-cache');
if (!mjxDefs) {
// Fallback: try to find defs in any SVG on the page
const allDefs = document.querySelectorAll('svg defs');
if (allDefs.length > 0) {
mjxDefs = allDefs[allDefs.length - 1]; // Get the last one (likely the font cache)
}
}
// Convert MathJax equations to SVG for PDF with proper dimensions
const mjxContainers = previewClone.querySelectorAll('mjx-container');
mjxContainers.forEach((container, i) => {
const svg = container.querySelector('svg');
if (svg && dimensions[i]) {
const { width, height } = dimensions[i];
// Set explicit dimensions
svg.setAttribute('width', width + 'px');
svg.setAttribute('height', height + 'px');
// Ensure xmlns
if (!svg.hasAttribute('xmlns')) {
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
}
// Add MathJax font definitions if found and not already present
if (mjxDefs && !svg.querySelector('defs')) {
const defsClone = mjxDefs.cloneNode(true);
svg.insertBefore(defsClone, svg.firstChild);
}
// Create wrapper
const wrapper = document.createElement('div');
wrapper.style.display = 'inline-block';
wrapper.style.verticalAlign = 'middle';
if (container.hasAttribute('display') || window.getComputedStyle(container).display === 'block') {
wrapper.style.display = 'block';
wrapper.style.textAlign = 'center';
wrapper.style.margin = '1em 0';
}
wrapper.appendChild(svg.cloneNode(true));
container.replaceWith(wrapper);
}
});
// Get the HTML with SVG equations
const html = previewClone.innerHTML;
// Send to backend for PDF generation
const response = await fetch('/export-pdf', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ html: html })
});
if (!response.ok) {
throw new Error('PDF generation failed');
}
// Download the PDF
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'markdown-export.pdf';
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
console.error('Error generating PDF:', error);
alert('Failed to generate PDF. Please try again.');
} finally {
button.disabled = false;
button.textContent = originalText;
}
});
// Synchronized scrolling
const editorElement = document.getElementById('editor');
const previewPane = document.querySelector('.preview-pane');
let syncTimeout = null;
let isEditorScrolling = false;
let isPreviewScrolling = false;
// Sync preview scroll with editor scroll
editorElement.addEventListener('scroll', () => {
if (isPreviewScrolling) return;
clearTimeout(syncTimeout);
isEditorScrolling = true;
const sourceScrollHeight = editorElement.scrollHeight - editorElement.clientHeight;
const targetScrollHeight = previewPane.scrollHeight - previewPane.clientHeight;
if (sourceScrollHeight > 0 && targetScrollHeight > 0) {
const scrollPercentage = editorElement.scrollTop / sourceScrollHeight;
previewPane.scrollTop = scrollPercentage * targetScrollHeight;
}
syncTimeout = setTimeout(() => {
isEditorScrolling = false;
}, 50);
});
// Sync editor scroll with preview scroll
previewPane.addEventListener('scroll', () => {
if (isEditorScrolling) return;
clearTimeout(syncTimeout);
isPreviewScrolling = true;
const sourceScrollHeight = previewPane.scrollHeight - previewPane.clientHeight;
const targetScrollHeight = editorElement.scrollHeight - editorElement.clientHeight;
if (sourceScrollHeight > 0 && targetScrollHeight > 0) {
const scrollPercentage = previewPane.scrollTop / sourceScrollHeight;
editorElement.scrollTop = scrollPercentage * targetScrollHeight;
}
syncTimeout = setTimeout(() => {
isPreviewScrolling = false;
}, 50);
});
// Initial render
renderMarkdown();
</script>
</body>
</html>