Created
May 23, 2025 11:02
-
-
Save fedir/689f1f7f68fb730ad0349cecffb86f11 to your computer and use it in GitHub Desktop.
Simple one-page HTML/JS #Markdown converter using marked.js
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: white; | |
color: #333; | |
padding: 1rem 2rem; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
box-shadow: 0 2px 10px rgba(0,0,0,0.1); | |
border-bottom: 1px solid #ddd; | |
} | |
.dark-mode .header { | |
background: #2a2a2a; | |
color: #e0e0e0; | |
border-bottom-color: #444; | |
} | |
.header h1 { | |
font-size: 1.5rem; | |
font-weight: 600; | |
} | |
.header-controls { | |
display: flex; | |
gap: 1rem; | |
align-items: center; | |
} | |
.btn { | |
background: #f8f9fa; | |
color: #333; | |
border: 1px solid #ddd; | |
padding: 0.5rem 1rem; | |
border-radius: 6px; | |
cursor: pointer; | |
font-size: 0.9rem; | |
transition: all 0.2s ease; | |
} | |
.btn:hover { | |
background: #e9ecef; | |
transform: translateY(-1px); | |
} | |
.dark-mode .btn { | |
background: #3a3a3a; | |
color: #e0e0e0; | |
border-color: #555; | |
} | |
.dark-mode .btn:hover { | |
background: #4a4a4a; | |
} | |
.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> | |
</div> | |
</header> | |
<div class="main-container"> | |
<div class="panel"> | |
<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-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()); | |
// 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 { | |
let html = marked.parse(markdownText); | |
// 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 previewElement = this.preview; | |
if (!previewElement || previewElement.innerHTML.includes('empty-state')) { | |
this.showNotification('Nothing to copy', 'error'); | |
return; | |
} | |
try { | |
// Create a range and selection to copy the rendered content | |
const range = document.createRange(); | |
range.selectNodeContents(previewElement); | |
const selection = window.getSelection(); | |
selection.removeAllRanges(); | |
selection.addRange(range); | |
// Copy the selected content | |
document.execCommand('copy'); | |
selection.removeAllRanges(); | |
this.showNotification('✅ Content copied to clipboard'); | |
} catch (error) { | |
// Fallback method | |
try { | |
await navigator.clipboard.writeText(previewElement.textContent); | |
this.showNotification('✅ Text 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