Skip to content

Instantly share code, notes, and snippets.

@fedir
Created May 23, 2025 11:02
Show Gist options
  • Save fedir/689f1f7f68fb730ad0349cecffb86f11 to your computer and use it in GitHub Desktop.
Save fedir/689f1f7f68fb730ad0349cecffb86f11 to your computer and use it in GitHub Desktop.
Simple one-page HTML/JS #Markdown converter using marked.js
<!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