755 lines
25 KiB
HTML
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>
|