Last active
September 7, 2025 22:24
-
-
Save fedir/689f1f7f68fb730ad0349cecffb86f11 to your computer and use it in GitHub Desktop.
Simple one-page HTML/JS #Markdown converter using marked.js (handles Gemini's broken <br> tags). Demo : https://www.fedir.fr/tools/mdconv.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Markdown to Google Docs Converter</title> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.3.0/marked.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.5/purify.min.js"></script> | |
<style> | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
} | |
body { | |
font-family: system-ui, -apple-system, Arial, sans-serif; | |
background-color: #f5f5f5; | |
color: #333; | |
height: 100vh; | |
display: flex; | |
flex-direction: column; | |
transition: all 0.3s ease; | |
} | |
body.dark-mode { | |
background-color: #1a1a1a; | |
color: #e0e0e0; | |
} | |
.header { | |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
color: white; | |
padding: 1rem 2rem; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
box-shadow: 0 2px 10px rgba(0,0,0,0.1); | |
} | |
.header h1 { | |
font-size: 1.5rem; | |
font-weight: 600; | |
} | |
.header-controls { | |
display: flex; | |
gap: 1rem; | |
align-items: center; | |
} | |
.btn { | |
background: rgba(255,255,255,0.2); | |
color: white; | |
border: 1px solid rgba(255,255,255,0.3); | |
padding: 0.5rem 1rem; | |
border-radius: 6px; | |
cursor: pointer; | |
font-size: 0.9rem; | |
transition: all 0.2s ease; | |
backdrop-filter: blur(10px); | |
} | |
.btn:hover { | |
background: rgba(255,255,255,0.3); | |
transform: translateY(-1px); | |
} | |
.btn:active { | |
transform: translateY(0); | |
} | |
.btn.primary { | |
background: #4CAF50; | |
border-color: #45a049; | |
} | |
.btn.primary:hover { | |
background: #45a049; | |
} | |
.theme-toggle { | |
background: none; | |
border: none; | |
color: white; | |
font-size: 1.2rem; | |
cursor: pointer; | |
padding: 0.5rem; | |
border-radius: 50%; | |
transition: all 0.2s ease; | |
} | |
.theme-toggle:hover { | |
background: rgba(255,255,255,0.2); | |
} | |
.main-container { | |
display: flex; | |
flex: 1; | |
overflow: hidden; | |
} | |
.panel { | |
flex: 1; | |
display: flex; | |
flex-direction: column; | |
border-right: 1px solid #ddd; | |
} | |
.dark-mode .panel { | |
border-right-color: #444; | |
} | |
.panel:last-child { | |
border-right: none; | |
} | |
.panel-header { | |
background: white; | |
padding: 1rem; | |
border-bottom: 1px solid #ddd; | |
font-weight: 600; | |
color: #666; | |
} | |
.dark-mode .panel-header { | |
background: #2a2a2a; | |
border-bottom-color: #444; | |
color: #ccc; | |
} | |
.panel-content { | |
flex: 1; | |
overflow: auto; | |
} | |
#markdown-input { | |
width: 100%; | |
height: 100%; | |
padding: 1.5rem; | |
border: none; | |
outline: none; | |
font-family: 'Courier New', monospace; | |
font-size: 14px; | |
line-height: 1.6; | |
resize: none; | |
background: white; | |
color: #333; | |
} | |
.dark-mode #markdown-input { | |
background: #2a2a2a; | |
color: #e0e0e0; | |
} | |
#preview { | |
padding: 2rem; | |
background: white; | |
min-height: 100%; | |
font-family: Arial, sans-serif; | |
line-height: 1.6; | |
color: #333; | |
} | |
.dark-mode #preview { | |
background: #2a2a2a; | |
color: #e0e0e0; | |
} | |
/* Google Docs compatible styles */ | |
#preview h1, #preview h2, #preview h3, #preview h4, #preview h5, #preview h6 { | |
font-family: Arial, sans-serif; | |
font-weight: normal; | |
margin: 1em 0 0.5em 0; | |
color: inherit; | |
} | |
#preview h1 { font-size: 20pt; font-weight: bold; } | |
#preview h2 { font-size: 16pt; font-weight: bold; } | |
#preview h3 { font-size: 14pt; font-weight: bold; } | |
#preview h4 { font-size: 12pt; font-weight: bold; } | |
#preview h5 { font-size: 11pt; font-weight: bold; } | |
#preview h6 { font-size: 10pt; font-weight: bold; } | |
#preview p { | |
margin: 0 0 1em 0; | |
font-size: 11pt; | |
} | |
#preview ul, #preview ol { | |
margin: 0 0 1em 0; | |
padding-left: 2em; | |
} | |
#preview li { | |
margin: 0.25em 0; | |
font-size: 11pt; | |
} | |
#preview table { | |
border-collapse: collapse; | |
width: 100%; | |
margin: 1em 0; | |
font-size: 11pt; | |
} | |
#preview th, #preview td { | |
border: 1px solid #ccc; | |
padding: 8px 12px; | |
text-align: left; | |
} | |
.dark-mode #preview th, | |
.dark-mode #preview td { | |
border-color: #555; | |
} | |
#preview th { | |
background-color: #f8f9fa; | |
font-weight: bold; | |
} | |
.dark-mode #preview th { | |
background-color: #3a3a3a; | |
} | |
#preview code { | |
background-color: #f1f3f4; | |
padding: 2px 4px; | |
border-radius: 3px; | |
font-family: 'Courier New', monospace; | |
font-size: 10pt; | |
} | |
.dark-mode #preview code { | |
background-color: #3a3a3a; | |
} | |
#preview pre { | |
background-color: #f8f9fa; | |
padding: 1em; | |
border-radius: 6px; | |
overflow-x: auto; | |
margin: 1em 0; | |
} | |
.dark-mode #preview pre { | |
background-color: #3a3a3a; | |
} | |
#preview pre code { | |
background: none; | |
padding: 0; | |
} | |
#preview blockquote { | |
margin: 1em 0; | |
padding-left: 1em; | |
border-left: 4px solid #ddd; | |
color: #666; | |
} | |
.dark-mode #preview blockquote { | |
border-left-color: #555; | |
color: #aaa; | |
} | |
#preview a { | |
color: #1a73e8; | |
text-decoration: underline; | |
} | |
.dark-mode #preview a { | |
color: #8ab4f8; | |
} | |
#preview img { | |
max-width: 100%; | |
height: auto; | |
margin: 1em 0; | |
} | |
.notification { | |
position: fixed; | |
top: 20px; | |
right: 20px; | |
background: #4CAF50; | |
color: white; | |
padding: 1rem 1.5rem; | |
border-radius: 6px; | |
box-shadow: 0 4px 12px rgba(0,0,0,0.15); | |
transform: translateX(400px); | |
transition: all 0.3s ease; | |
z-index: 1000; | |
} | |
.notification.show { | |
transform: translateX(0); | |
} | |
.notification.error { | |
background: #f44336; | |
} | |
.resize-handle { | |
width: 4px; | |
background: #ddd; | |
cursor: col-resize; | |
transition: background 0.2s ease; | |
position: relative; | |
} | |
.resize-handle:hover { | |
background: #999; | |
} | |
.dark-mode .resize-handle { | |
background: #444; | |
} | |
.dark-mode .resize-handle:hover { | |
background: #666; | |
} | |
@media (max-width: 768px) { | |
.main-container { | |
flex-direction: column; | |
} | |
.panel { | |
border-right: none; | |
border-bottom: 1px solid #ddd; | |
} | |
.panel:last-child { | |
border-bottom: none; | |
} | |
.dark-mode .panel { | |
border-bottom-color: #444; | |
} | |
.header { | |
padding: 1rem; | |
} | |
.header h1 { | |
font-size: 1.2rem; | |
} | |
.header-controls { | |
gap: 0.5rem; | |
} | |
.btn { | |
padding: 0.4rem 0.8rem; | |
font-size: 0.8rem; | |
} | |
.resize-handle { | |
display: none; | |
} | |
} | |
.empty-state { | |
color: #999; | |
text-align: center; | |
padding: 3rem; | |
font-style: italic; | |
} | |
.placeholder-text { | |
color: #999; | |
font-style: italic; | |
pointer-events: none; | |
position: absolute; | |
top: 1.5rem; | |
left: 1.5rem; | |
} | |
.input-container { | |
position: relative; | |
height: 100%; | |
} | |
#markdown-input:focus + .placeholder-text, | |
#markdown-input:not(:empty) + .placeholder-text { | |
display: none; | |
} | |
</style> | |
</head> | |
<body> | |
<header class="header"> | |
<h1>📝 Markdown to Google Docs Converter</h1> | |
<div class="header-controls"> | |
<button id="clear-btn" class="btn" title="Clear all content">Clear</button> | |
<button id="copy-btn" class="btn primary" title="Copy HTML to clipboard">📋 Copy HTML</button> | |
<button class="theme-toggle" id="theme-toggle" title="Toggle dark mode">🌙</button> | |
</div> | |
</header> | |
<div class="main-container"> | |
<div class="panel"> | |
<div class="panel-header"> | |
✏️ Markdown Input | |
</div> | |
<div class="panel-content"> | |
<div class="input-container"> | |
<textarea id="markdown-input" placeholder="Type your Markdown here... | |
Example: | |
# My Document | |
## Introduction | |
This is a **bold** text and this is *italic*. | |
### Features | |
- Easy to use | |
- Real-time preview | |
- Google Docs compatible | |
### Table Example | |
| Feature | Status | | |
|---------|--------| | |
| Tables | ✅ | | |
| Lists | ✅ | | |
| Links | ✅ | | |
For more info, visit [Google Docs](https://docs.google.com). | |
```javascript | |
console.log('Code blocks work too!'); | |
``` | |
> This is a blockquote example. | |
"></textarea> | |
</div> | |
</div> | |
</div> | |
<div class="resize-handle" id="resize-handle"></div> | |
<div class="panel"> | |
<div class="panel-header"> | |
👁️ Preview (Google Docs Compatible) | |
</div> | |
<div class="panel-content"> | |
<div id="preview"> | |
<div class="empty-state"> | |
Your rendered Markdown will appear here... | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div id="notification" class="notification"></div> | |
<script> | |
class MarkdownConverter { | |
constructor() { | |
this.initializeElements(); | |
this.setupEventListeners(); | |
this.initializeMarked(); | |
this.loadFromStorage(); | |
this.setupResizer(); | |
// Initial render if there's placeholder content | |
if (this.markdownInput.value.trim()) { | |
this.renderMarkdown(); | |
} | |
} | |
initializeElements() { | |
this.markdownInput = document.getElementById('markdown-input'); | |
this.preview = document.getElementById('preview'); | |
this.copyBtn = document.getElementById('copy-btn'); | |
this.clearBtn = document.getElementById('clear-btn'); | |
this.notification = document.getElementById('notification'); | |
this.resizeHandle = document.getElementById('resize-handle'); | |
this.debounceTimer = null; | |
this.isResizing = false; | |
} | |
initializeMarked() { | |
// Configure marked.js for better HTML output | |
marked.setOptions({ | |
gfm: true, | |
breaks: true, | |
sanitize: false, | |
highlight: null | |
}); | |
} | |
setupEventListeners() { | |
// Debounced input rendering | |
this.markdownInput.addEventListener('input', () => { | |
clearTimeout(this.debounceTimer); | |
this.debounceTimer = setTimeout(() => { | |
this.renderMarkdown(); | |
this.saveToStorage(); | |
}, 300); | |
}); | |
// Button events | |
this.copyBtn.addEventListener('click', () => this.copyToClipboard()); | |
this.clearBtn.addEventListener('click', () => this.clearContent()); | |
this.themeToggle.addEventListener('click', () => this.toggleTheme()); | |
// Keyboard shortcuts | |
document.addEventListener('keydown', (e) => { | |
if ((e.ctrlKey || e.metaKey) && e.key === 'k') { | |
e.preventDefault(); | |
this.clearContent(); | |
} | |
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { | |
e.preventDefault(); | |
this.copyToClipboard(); | |
} | |
}); | |
// Theme detection | |
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { | |
document.body.classList.add('dark-mode'); | |
this.themeToggle.textContent = '☀️'; | |
} | |
} | |
setupResizer() { | |
let startX, startLeftWidth; | |
this.resizeHandle.addEventListener('mousedown', (e) => { | |
this.isResizing = true; | |
startX = e.clientX; | |
const leftPanel = document.querySelector('.panel:first-child'); | |
startLeftWidth = parseInt(window.getComputedStyle(leftPanel).width, 10); | |
document.addEventListener('mousemove', this.handleResize); | |
document.addEventListener('mouseup', this.stopResize); | |
document.body.style.cursor = 'col-resize'; | |
e.preventDefault(); | |
}); | |
} | |
handleResize = (e) => { | |
if (!this.isResizing) return; | |
const containerWidth = document.querySelector('.main-container').offsetWidth; | |
const deltaX = e.clientX - startX; | |
const newLeftWidth = startLeftWidth + deltaX; | |
const leftPercent = (newLeftWidth / containerWidth) * 100; | |
const rightPercent = 100 - leftPercent; | |
if (leftPercent >= 20 && rightPercent >= 20) { | |
const panels = document.querySelectorAll('.panel'); | |
panels[0].style.flex = `0 0 ${leftPercent}%`; | |
panels[1].style.flex = `0 0 ${rightPercent}%`; | |
} | |
} | |
stopResize = () => { | |
this.isResizing = false; | |
document.removeEventListener('mousemove', this.handleResize); | |
document.removeEventListener('mouseup', this.stopResize); | |
document.body.style.cursor = ''; | |
} | |
renderMarkdown() { | |
const markdownText = this.markdownInput.value.trim(); | |
if (!markdownText) { | |
this.preview.innerHTML = '<div class="empty-state">Your rendered Markdown will appear here...</div>'; | |
return; | |
} | |
try { | |
// Pre-process the markdown to handle common issues | |
let processedText = markdownText; | |
// Handle escaped HTML tags - convert to spaces within table context | |
processedText = processedText.replace(/\\<br\\>/gi, ' <br> '); | |
processedText = processedText.replace(/\\<br\d*\\>/gi, ' <br> '); | |
// Handle unescaped br tags | |
processedText = processedText.replace(/<br\s*\/?>/gi, ' <br> '); | |
processedText = processedText.replace(/<br\d+>/gi, ' <br> '); | |
let html = marked.parse(processedText); | |
// Post-process the HTML - only convert <br> to newlines outside of tables | |
// Inside table cells, keep <br> tags but clean them up | |
html = html.replace(/<br\s*\/?>/gi, '<br>'); | |
// Clean up any remaining artifacts | |
html = html.replace(/\\<br\\>/gi, ''); | |
html = html.replace(/<br>/gi, ''); | |
// Sanitize if DOMPurify is available | |
if (typeof DOMPurify !== 'undefined') { | |
html = DOMPurify.sanitize(html); | |
} | |
this.preview.innerHTML = html; | |
} catch (error) { | |
console.error('Markdown parsing error:', error); | |
this.showNotification('Error parsing Markdown', 'error'); | |
} | |
} | |
async copyToClipboard() { | |
const content = this.preview.innerHTML; | |
if (!content || content.includes('empty-state')) { | |
this.showNotification('Nothing to copy', 'error'); | |
return; | |
} | |
try { | |
// Create a temporary container with Google Docs compatible styles | |
const tempDiv = document.createElement('div'); | |
tempDiv.innerHTML = content; | |
tempDiv.style.fontFamily = 'Arial, sans-serif'; | |
tempDiv.style.fontSize = '11pt'; | |
tempDiv.style.lineHeight = '1.6'; | |
// Copy as both HTML and plain text | |
const clipboardItem = new ClipboardItem({ | |
'text/html': new Blob([tempDiv.outerHTML], { type: 'text/html' }), | |
'text/plain': new Blob([tempDiv.textContent], { type: 'text/plain' }) | |
}); | |
await navigator.clipboard.write([clipboardItem]); | |
this.showNotification('✅ Copied! Paste into Google Docs'); | |
} catch (error) { | |
// Fallback for older browsers | |
try { | |
await navigator.clipboard.writeText(content); | |
this.showNotification('✅ HTML copied to clipboard'); | |
} catch (fallbackError) { | |
console.error('Copy failed:', fallbackError); | |
this.showNotification('Copy failed. Please select and copy manually.', 'error'); | |
} | |
} | |
} | |
clearContent() { | |
this.markdownInput.value = ''; | |
this.preview.innerHTML = '<div class="empty-state">Your rendered Markdown will appear here...</div>'; | |
this.markdownInput.focus(); | |
this.saveToStorage(); | |
this.showNotification('Content cleared'); | |
} | |
toggleTheme() { | |
document.body.classList.toggle('dark-mode'); | |
const isDark = document.body.classList.contains('dark-mode'); | |
this.themeToggle.textContent = isDark ? '☀️' : '🌙'; | |
// Save theme preference | |
try { | |
// Note: localStorage not available in Claude artifacts | |
console.log('Theme toggled to:', isDark ? 'dark' : 'light'); | |
} catch (e) { | |
// Ignore storage errors | |
} | |
} | |
showNotification(message, type = 'success') { | |
this.notification.textContent = message; | |
this.notification.className = `notification ${type} show`; | |
setTimeout(() => { | |
this.notification.classList.remove('show'); | |
}, 3000); | |
} | |
saveToStorage() { | |
try { | |
// Note: localStorage not available in Claude artifacts | |
console.log('Content would be saved to localStorage'); | |
} catch (e) { | |
// Ignore storage errors in Claude environment | |
} | |
} | |
loadFromStorage() { | |
try { | |
// Note: localStorage not available in Claude artifacts | |
console.log('Content would be loaded from localStorage'); | |
} catch (e) { | |
// Ignore storage errors in Claude environment | |
} | |
} | |
} | |
// Initialize the application when the DOM is loaded | |
document.addEventListener('DOMContentLoaded', () => { | |
new MarkdownConverter(); | |
}); | |
// Add some helpful sample content on first load | |
window.addEventListener('load', () => { | |
const input = document.getElementById('markdown-input'); | |
if (!input.value || input.value === input.getAttribute('placeholder')) { | |
// The placeholder content is already set in the HTML | |
setTimeout(() => { | |
const converter = new MarkdownConverter(); | |
converter.renderMarkdown(); | |
}, 100); | |
} | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment