import React, { useState, useRef, useEffect } from 'react'; import { FileText, Plus, Upload, Download, Globe, AlertTriangle, Edit3, Eye, EyeOff, Columns, Type, Maximize2, Minimize2, Bold, Italic, Underline, List, ListOrdered, Link2, Code, Table, Minus, Heading, Quote, CheckSquare, ChevronUp, ChevronDown, FileDown } from 'lucide-react'; import ToolLayout from '../components/ToolLayout'; import CodeMirrorEditor from '../components/CodeMirrorEditor'; import SEO from '../components/SEO'; import RelatedTools from '../components/RelatedTools'; import { marked } from 'marked'; import { markedEmoji } from 'marked-emoji'; import DOMPurify from 'dompurify'; import hljs from 'highlight.js'; import html2pdf from 'html2pdf.js'; import 'highlight.js/styles/github-dark.css'; import '../styles/markdown-preview.css'; const MarkdownEditor = () => { const [markdownText, setMarkdownText] = useState(''); // Sync markdown data to localStorage for navigation guard useEffect(() => { localStorage.setItem('markdownEditorData', markdownText); }, [markdownText]); // State management following ObjectEditor pattern const [activeTab, setActiveTab] = useState('create'); const [inputText, setInputText] = useState(''); const [error, setError] = useState(''); // Default to 'editor' on mobile, 'split' on desktop const [viewMode, setViewMode] = useState(() => { return window.innerWidth < 1024 ? 'editor' : 'split'; }); const [isFullscreen, setIsFullscreen] = useState(false); const [fetchUrl, setFetchUrl] = useState(''); const [fetching, setFetching] = useState(false); const [showHeadingDropdown, setShowHeadingDropdown] = useState(false); const [createNewCompleted, setCreateNewCompleted] = useState(false); const [showInputChangeModal, setShowInputChangeModal] = useState(false); const [pendingTabChange, setPendingTabChange] = useState(null); const fileInputRef = useRef(null); const [pasteCollapsed, setPasteCollapsed] = useState(false); const [pasteDataSummary, setPasteDataSummary] = useState(null); const [urlDataSummary, setUrlDataSummary] = useState(null); const [fileDataSummary, setFileDataSummary] = useState(null); const [usageTipsExpanded, setUsageTipsExpanded] = useState(false); // Configure marked with custom renderer for code blocks useEffect(() => { const renderer = new marked.Renderer(); // Custom code block renderer with header and copy button renderer.code = function(token) { // In marked v4+, parameters come as an object const codeString = String(token.text || token || ''); const language = token.lang || ''; const normalizedLang = language ? language.toLowerCase().trim() : ''; let highlightedCode = codeString; // Apply syntax highlighting if (normalizedLang && hljs.getLanguage(normalizedLang)) { try { const result = hljs.highlight(codeString, { language: normalizedLang }); highlightedCode = result.value; } catch (e) { highlightedCode = codeString; } } else { try { const result = hljs.highlightAuto(codeString); highlightedCode = result.value; } catch (e) { highlightedCode = codeString; } } const displayLang = normalizedLang || 'text'; // Create a unique ID for this code block const blockId = 'code-' + Math.random().toString(36).substr(2, 9); return `
${highlightedCode}
Error parsing markdown
'; } }; // Calculate statistics const calculateStats = () => { if (!markdownText) { return { words: 0, characters: 0, lines: 0, readingTime: 0 }; } const words = markdownText.trim().split(/\s+/).length; const characters = markdownText.length; const lines = markdownText.split('\n').length; const readingTime = Math.ceil(words / 200); return { words, characters, lines, readingTime }; }; const stats = calculateStats(); // Add event delegation for copy buttons and close dropdown on outside click useEffect(() => { const handleClick = (e) => { // Handle copy button clicks const button = e.target.closest('.code-block-copy'); if (button) { const codeId = button.getAttribute('data-code-id'); const codeElement = document.getElementById(codeId); if (codeElement) { const code = codeElement.textContent; navigator.clipboard.writeText(code).then(() => { const originalText = button.textContent; button.textContent = 'Copied!'; setTimeout(() => { button.textContent = originalText; }, 2000); }).catch(err => { // Failed to copy }); } return; } // Close heading dropdown if clicking outside if (showHeadingDropdown && !e.target.closest('.relative')) { setShowHeadingDropdown(false); } }; document.addEventListener('click', handleClick); return () => document.removeEventListener('click', handleClick); }, [showHeadingDropdown]); // Toolbar formatting functions - proper CodeMirror integration with toggle support const insertMarkdown = (before, after = '', placeholder = 'text', skipPlaceholder = false) => { // Get CodeMirror view from the DOM const editorElement = document.querySelector('.cm-editor'); if (!editorElement) return; const view = editorElement.cmView?.view; if (!view) return; const state = view.state; const selection = state.selection.main; // Get selected text or use placeholder (unless skipPlaceholder is true) const selectedText = state.doc.sliceString(selection.from, selection.to) || (skipPlaceholder ? '' : placeholder); // Check if text is already formatted (toggle support) const beforeLen = before.length; const afterLen = after.length; const expandedFrom = Math.max(0, selection.from - beforeLen); const expandedTo = Math.min(state.doc.length, selection.to + afterLen); const expandedText = state.doc.sliceString(expandedFrom, expandedTo); // Check if already formatted const isFormatted = expandedText.startsWith(before) && expandedText.endsWith(after); if (isFormatted && selectedText) { // Remove formatting view.dispatch({ changes: { from: expandedFrom, to: expandedTo, insert: selectedText }, selection: { anchor: expandedFrom, head: expandedFrom + selectedText.length } }); } else { // Add formatting const formatted = `${before}${selectedText}${after}`; view.dispatch({ changes: { from: selection.from, to: selection.to, insert: formatted }, selection: { anchor: selection.from + before.length, head: selection.from + before.length + selectedText.length } }); } // Focus back to editor view.focus(); }; // Heading insertion helper const insertHeading = (level) => { const prefix = '#'.repeat(level) + ' '; insertMarkdown(prefix, '', 'Heading'); setShowHeadingDropdown(false); }; const toolbarButtons = [ { icon: Heading, label: 'Heading', action: () => setShowHeadingDropdown(!showHeadingDropdown), isDropdown: true, group: 'formatter' }, { icon: Bold, label: 'Bold', action: () => insertMarkdown('**', '**', 'bold text'), group: 'formatter' }, { icon: Italic, label: 'Italic', action: () => insertMarkdown('*', '*', 'italic text'), group: 'formatter' }, { icon: Underline, label: 'Underline', action: () => insertMarkdown('__', '__', 'underlined text'), group: 'formatter' }, { icon: Quote, label: 'Quote', action: () => insertMarkdown('> ', '', 'quote'), group: 'formatter' }, { icon: Code, label: 'Code Block', action: () => insertMarkdown('\n```\n', '\n```\n', 'code'), group: 'formatter' }, { icon: Link2, label: 'Link', action: () => insertMarkdown('[', '](url)', 'link text'), group: 'formatter' }, { icon: List, label: 'Bullet List', action: () => insertMarkdown('- ', '', 'list item'), group: 'list' }, { icon: ListOrdered, label: 'Numbered List', action: () => insertMarkdown('1. ', '', 'list item'), group: 'list' }, { icon: CheckSquare, label: 'Task List', action: () => insertMarkdown('- [ ] ', '', 'task'), group: 'list' }, { icon: Minus, label: 'Divider', action: () => insertMarkdown('\n---\n', '', '', true), group: 'element' }, { icon: Table, label: 'Table', action: () => insertMarkdown('\n| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |', '', '', true), group: 'element' }, ]; // Sample markdown const sampleMarkdown = `# Welcome to Markdown Editor A powerful, privacy-first markdown editor with live preview. ## Features - **Live Preview** - See your markdown rendered in real-time - **Syntax Highlighting** - Beautiful code blocks with syntax highlighting - **Export Options** - Export to Markdown, HTML, or Plain Text - **GitHub Flavored Markdown** - Full GFM support ## Code Examples with Syntax Highlighting JavaScript: \`\`\`javascript function greet(name) { console.log(\`Hello, \${name}!\`); } greet('World'); \`\`\` Bash: \`\`\`bash #!/bin/bash echo "Hello, World!" npm install \`\`\` JSON: \`\`\`json { "name": "markdown-editor", "version": "1.0.0", "features": ["live-preview", "syntax-highlighting"] } \`\`\` PHP: \`\`\`php \`\`\` ## Tables | Feature | Status | |---------|--------| | Live Preview | ✅ | | Export | ✅ | | Privacy-First | ✅ | ## Links and Images [Visit our homepage](/) > This is a blockquote. Perfect for highlighting important information. --- **Bold text**, *italic text*, __underlined text__, and \`inline code\` are all supported! ### Lists 1. First item 2. Second item 3. Third item - Unordered item - Another item - Nested item - Another nested item Happy writing! 🚀`; // Markdown Templates const templates = { sample: sampleMarkdown, readme: `# Project Name ## Description A brief description of what this project does and who it's for. ## Features - ✨ Feature 1 - 🚀 Feature 2 - 💡 Feature 3 ## Installation \`\`\`bash npm install project-name \`\`\` ## Usage \`\`\`javascript const project = require('project-name'); project.doSomething(); \`\`\` ## API Reference ### \`functionName(param)\` Description of what the function does. **Parameters:** - \`param\` (string): Description of parameter **Returns:** Description of return value ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. ## License MIT License - see LICENSE file for details ## Contact - GitHub: [@username](https://github.com/username) - Email: email@example.com`, blog: `# Blog Post Title **Published:** ${new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })} **Author:** Your Name **Tags:** #tag1 #tag2 #tag3 --- ## Introduction Start with a compelling introduction that hooks your readers and explains what they'll learn from this post. ## Main Content ### Section 1: Key Point Explain your first main point here. Use examples and code snippets to illustrate: \`\`\`javascript // Example code const example = "This makes your point clear"; console.log(example); \`\`\` ### Section 2: Another Key Point Continue with your second main point. Break down complex ideas into digestible chunks. > 💡 **Pro Tip:** Use blockquotes to highlight important insights or tips. ### Section 3: Practical Application Show readers how to apply what they've learned: 1. Step one 2. Step two 3. Step three ## Conclusion Summarize the key takeaways and provide next steps or additional resources. ## Resources - [Resource 1](https://example.com) - [Resource 2](https://example.com) - [Resource 3](https://example.com) --- *Thanks for reading! If you found this helpful, please share it with others.*`, documentation: `# API Documentation ## Overview Brief description of the API and its purpose. ## Base URL \`\`\` https://api.example.com/v1 \`\`\` ## Authentication All API requests require authentication using an API key: \`\`\`bash curl -H "Authorization: Bearer YOUR_API_KEY" https://api.example.com/v1/endpoint \`\`\` ## Endpoints ### GET /users Retrieve a list of users. **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | page | integer | No | Page number (default: 1) | | limit | integer | No | Items per page (default: 10) | **Response:** \`\`\`json { "data": [ { "id": 1, "name": "John Doe", "email": "john@example.com" } ], "meta": { "page": 1, "total": 100 } } \`\`\` ### POST /users Create a new user. **Request Body:** \`\`\`json { "name": "Jane Doe", "email": "jane@example.com", "password": "secure_password" } \`\`\` **Response:** \`\`\`json { "id": 2, "name": "Jane Doe", "email": "jane@example.com", "created_at": "2025-01-01T00:00:00Z" } \`\`\` ## Error Codes | Code | Description | |------|-------------| | 400 | Bad Request - Invalid parameters | | 401 | Unauthorized - Invalid API key | | 404 | Not Found - Resource doesn't exist | | 500 | Internal Server Error | ## Rate Limiting - 1000 requests per hour per API key - Rate limit info included in response headers ## Support For support, email support@example.com or visit our [Help Center](https://help.example.com).`, meeting: `# Meeting Notes **Date:** ${new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })} **Time:** [Start Time] - [End Time] **Location:** [Location/Video Call Link] ## Attendees - [ ] Person 1 - [ ] Person 2 - [ ] Person 3 ## Agenda 1. Review previous action items 2. Topic 1 3. Topic 2 4. Any other business --- ## Discussion ### Topic 1: [Topic Name] **Presenter:** [Name] **Key Points:** - Point 1 - Point 2 - Point 3 **Decisions Made:** - Decision 1 - Decision 2 ### Topic 2: [Topic Name] **Presenter:** [Name] **Key Points:** - Point 1 - Point 2 ## Action Items | Task | Owner | Due Date | Status | |------|-------|----------|--------| | Task 1 | Person A | 2025-01-15 | 🔄 In Progress | | Task 2 | Person B | 2025-01-20 | ⏳ Pending | | Task 3 | Person C | 2025-01-10 | ✅ Complete | ## Next Meeting **Date:** [Next Meeting Date] **Agenda Items:** - Follow up on action items - [Other topics] --- *Notes prepared by: [Your Name]*`, changelog: `# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ### Added - New features that have been added ### Changed - Changes in existing functionality ### Deprecated - Soon-to-be removed features ### Removed - Removed features ### Fixed - Bug fixes ### Security - Security improvements ## [1.0.0] - ${new Date().toISOString().split('T')[0]} ### Added - Initial release - Core functionality implemented - Documentation added - Tests added ### Changed - Updated dependencies - Improved performance ### Fixed - Fixed bug #123 - Resolved issue with feature X ## [0.2.0] - 2024-12-01 ### Added - Feature A - Feature B ### Changed - Refactored module X - Updated UI components ### Fixed - Fixed critical bug in authentication - Resolved memory leak ## [0.1.0] - 2024-11-01 ### Added - Initial beta release - Basic features implemented - Project structure established --- [Unreleased]: https://github.com/username/repo/compare/v1.0.0...HEAD [1.0.0]: https://github.com/username/repo/compare/v0.2.0...v1.0.0 [0.2.0]: https://github.com/username/repo/compare/v0.1.0...v0.2.0 [0.1.0]: https://github.com/username/repo/releases/tag/v0.1.0` }; const [selectedTemplate, setSelectedTemplate] = useState('sample'); // Helper function to check if user has data that would be lost const hasUserData = () => { return markdownText.trim().length > 0; }; // Check if current data has been modified from initial state const hasModifiedData = () => { if (!markdownText.trim()) return false; if (markdownText === sampleMarkdown) return false; return true; }; // Handle tab change with confirmation if data exists const handleTabChange = (newTab) => { if (newTab === 'create' && activeTab !== 'create') { if (hasModifiedData()) { setPendingTabChange(newTab); setShowInputChangeModal(true); } else { setActiveTab(newTab); setCreateNewCompleted(false); } } else if (hasUserData() && activeTab !== newTab) { setPendingTabChange(newTab); setShowInputChangeModal(true); } else { setActiveTab(newTab); if (newTab === 'create' && createNewCompleted) { setCreateNewCompleted(false); } } }; // Clear all data function const clearAllData = () => { setMarkdownText(''); setInputText(''); setError(''); setCreateNewCompleted(false); setPasteCollapsed(false); setPasteDataSummary(null); setUrlDataSummary(null); setFileDataSummary(null); }; // Confirm input method change and clear data const confirmInputChange = () => { if (pendingTabChange === 'create_empty') { clearAllData(); setMarkdownText(''); setCreateNewCompleted(true); } else if (pendingTabChange === 'create_sample') { clearAllData(); setMarkdownText(templates[selectedTemplate]); setCreateNewCompleted(true); } else { clearAllData(); setActiveTab(pendingTabChange); if (pendingTabChange === 'create') { setCreateNewCompleted(false); } } setShowInputChangeModal(false); setPendingTabChange(null); }; // Cancel input method change const cancelInputChange = () => { setShowInputChangeModal(false); setPendingTabChange(null); }; // Handle Parse Markdown button click const handleParseMarkdown = () => { if (!inputText.trim()) { setError('Please enter some markdown text'); setPasteCollapsed(false); return; } setMarkdownText(inputText); setError(''); setCreateNewCompleted(true); setPasteDataSummary({ format: 'Markdown', size: inputText.length, lines: inputText.split('\n').length }); setPasteCollapsed(true); }; // Handle file import (auto-load, same as ObjectEditor) const handleFileUpload = (event) => { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { let content = e.target.result; let format = 'Markdown'; // Detect file type and convert if needed if (file.name.endsWith('.html') || file.name.endsWith('.htm')) { // Extract content from HTML body if it's a full HTML document const bodyMatch = content.match(/]*>([\s\S]*)<\/body>/i); if (bodyMatch) { content = bodyMatch[1]; } // Convert HTML to markdown (basic conversion) content = htmlToMarkdown(content); format = 'HTML (converted)'; } else if (file.name.endsWith('.txt')) { // Plain text - treat as markdown-ready content format = 'Plain Text'; } setMarkdownText(content); setActiveTab('open'); setCreateNewCompleted(true); setError(''); // Update file data summary setFileDataSummary({ format: format, size: content.length, lines: content.split('\n').length, filename: file.name }); }; reader.onerror = () => { setError('Failed to read file'); }; reader.readAsText(file); }; // Fetch markdown from URL const handleFetchFromURL = async () => { if (!fetchUrl.trim()) { setError('Please enter a URL'); return; } setFetching(true); setError(''); try { let urlToFetch = fetchUrl.trim(); // Convert GitHub URLs to raw URLs if (urlToFetch.includes('github.com') && !urlToFetch.includes('raw.githubusercontent.com')) { urlToFetch = urlToFetch .replace('github.com', 'raw.githubusercontent.com') .replace('/blob/', '/'); } // Try direct fetch first let response; try { response = await fetch(urlToFetch); } catch (corsError) { // If CORS error, try with CORS proxy response = await fetch(`https://api.allorigins.win/raw?url=${encodeURIComponent(urlToFetch)}`); } if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const text = await response.text(); if (!text || text.trim().length === 0) { throw new Error('URL returned empty content'); } // Detect format let format = 'Markdown'; if (urlToFetch.endsWith('.html') || urlToFetch.endsWith('.htm')) { format = 'HTML (converted)'; const converted = htmlToMarkdown(text); setMarkdownText(converted); } else { setMarkdownText(text); } setActiveTab('url'); setCreateNewCompleted(true); setError(''); // Update URL data summary setUrlDataSummary({ format: format, size: text.length, lines: text.split('\n').length, url: urlToFetch }); } catch (err) { setError(`Failed to fetch from URL: ${err.message}`); } finally { setFetching(false); } }; // HTML to Markdown converter - designed to reverse our exact export format const htmlToMarkdown = (html) => { // Create a temporary DOM element to parse HTML properly const tempDiv = document.createElement('div'); tempDiv.innerHTML = html; // Remove script and style tags tempDiv.querySelectorAll('script, style').forEach(el => el.remove()); // Process our custom code-block-wrapper structure tempDiv.querySelectorAll('.code-block-wrapper').forEach(wrapper => { const langSpan = wrapper.querySelector('.code-block-language'); const codeElement = wrapper.querySelector('code'); if (codeElement) { const language = langSpan ? langSpan.textContent.trim() : ''; // Get the text content directly (this preserves the actual code) let code = codeElement.textContent; // Create markdown code block const codeBlock = document.createTextNode(`\n\`\`\`${language}\n${code}\n\`\`\`\n\n`); wrapper.parentNode.replaceChild(codeBlock, wrapper); } }); // Handle regular code blocks (without our wrapper) tempDiv.querySelectorAll('pre > code').forEach(codeElement => { const pre = codeElement.parentElement; const classMatch = codeElement.className.match(/language-(\w+)/); const language = classMatch ? classMatch[1] : ''; let code = codeElement.textContent; // Create markdown code block const codeBlock = document.createTextNode(`\n\`\`\`${language}\n${code}\n\`\`\`\n\n`); pre.parentNode.replaceChild(codeBlock, pre); }); // Get the processed HTML let markdown = tempDiv.innerHTML; // Headers markdown = markdown.replace(/]*>([\s\S]*?)<\/code>/gi, '`$1`');
// Tables
markdown = markdown.replace(/]*>([\s\S]*?)<\/table>/gi, (match, content) => {
let tableMarkdown = '\n';
const rows = content.match(/]*>([\s\S]*?)<\/tr>/gi) || [];
rows.forEach((row, index) => {
const cells = row.match(/]*>([\s\S]*?)<\/t[hd]>/gi) || [];
const cellContents = cells.map(cell => cell.replace(/<[^>]+>/g, '').trim());
tableMarkdown += '| ' + cellContents.join(' | ') + ' |\n';
// Add separator after header row
if (index === 0) {
tableMarkdown += '| ' + cellContents.map(() => '---').join(' | ') + ' |\n';
}
});
return tableMarkdown + '\n';
});
// Lists - handle nested lists
markdown = markdown.replace(/]*>([\s\S]*?)<\/li>/gi, (match, content) => {
content = content.trim();
return '- ' + content + '\n';
});
markdown = markdown.replace(/]*>([\s\S]*?)<\/ul>/gi, '$1\n');
markdown = markdown.replace(/]*>([\s\S]*?)<\/ol>/gi, '$1\n');
// Blockquotes
markdown = markdown.replace(/]*>([\s\S]*?)<\/blockquote>/gi, (match, content) => {
// Remove HTML tags from content first
content = content.replace(/<[^>]+>/g, '');
return '\n' + content.split('\n').filter(line => line.trim()).map(line => '> ' + line.trim()).join('\n') + '\n\n';
});
// Paragraphs and breaks
markdown = markdown.replace(/]*>([\s\S]*?)<\/p>/gi, (match, content) => {
content = content.trim();
return content + '\n\n';
});
markdown = markdown.replace(/
/gi, '\n');
markdown = markdown.replace(/
/gi, '\n---\n\n');
// Remove remaining HTML tags
markdown = markdown.replace(/<[^>]+>/g, '');
// Decode HTML entities (that might still be in non-code content)
markdown = markdown.replace(/</g, '<');
markdown = markdown.replace(/>/g, '>');
markdown = markdown.replace(/&/g, '&');
markdown = markdown.replace(/"/g, '"');
markdown = markdown.replace(/'/g, "'");
markdown = markdown.replace(/ /g, ' ');
// Clean up extra whitespace gently
// Remove multiple consecutive blank lines (3+ becomes 2)
markdown = markdown.replace(/\n{3,}/g, '\n\n');
// Remove leading spaces from each line (but preserve code block indentation)
let inCodeBlock = false;
markdown = markdown.split('\n').map(line => {
// Check if this line starts or ends a code block
if (line.trim().startsWith('```')) {
inCodeBlock = !inCodeBlock;
return line.trimStart(); // Trim the ``` line itself
}
// If we're inside a code block, preserve indentation
if (inCodeBlock) {
return line;
}
// Outside code blocks, remove leading spaces
return line.trimStart();
}).join('\n');
// Remove leading/trailing whitespace from the entire document
markdown = markdown.trim();
return markdown;
};
// Download file helper
const downloadFile = (content, filename, mimeType) => {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
// Export handlers
const handleExportMarkdown = () => {
if (!markdownText.trim()) {
setError('No content to export');
return;
}
downloadFile(markdownText, 'document.md', 'text/markdown');
};
const handleExportHTML = () => {
if (!markdownText.trim()) {
setError('No content to export');
return;
}
const html = parseMarkdown(markdownText);
const fullHTML = `
Markdown Document
${html}
`;
downloadFile(fullHTML, 'document.html', 'text/html');
};
const handleExportHTMLContent = () => {
if (!markdownText.trim()) {
setError('No content to export');
return;
}
let html = parseMarkdown(markdownText);
// Clean up the HTML content by removing code-block wrappers
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
// Remove code-block-header and code-block-wrapper elements
tempDiv.querySelectorAll('.code-block-header').forEach(el => el.remove());
tempDiv.querySelectorAll('.code-block-wrapper').forEach(wrapper => {
while (wrapper.firstChild) {
wrapper.parentNode.insertBefore(wrapper.firstChild, wrapper);
}
wrapper.remove();
});
html = tempDiv.innerHTML;
downloadFile(html, 'content.html', 'text/html');
};
const handleExportPlainText = () => {
if (!markdownText.trim()) {
setError('No content to export');
return;
}
// Export as markdown-ready plain text (keep markdown syntax)
// This allows users to copy/paste and re-import without losing formatting
downloadFile(markdownText, 'document.txt', 'text/plain');
};
const handleCopyToClipboard = () => {
if (!markdownText.trim()) {
setError('No content to copy');
return;
}
navigator.clipboard.writeText(markdownText).then(() => {
// Show success feedback (you can add a toast notification here)
setError(''); // Clear any errors
}).catch(() => {
setError('Failed to copy to clipboard');
});
};
const handleExportPDF = () => {
if (!markdownText.trim()) {
setError('No content to export');
return;
}
// Create the content element with rendered markdown
const element = document.createElement('div');
element.innerHTML = parseMarkdown(markdownText);
// Remove code block headers (language + copy button)
const codeHeaders = element.querySelectorAll('.code-block-header');
codeHeaders.forEach(header => header.remove());
// Remove copy buttons from code blocks
const copyButtons = element.querySelectorAll('button[title="Copy code"]');
copyButtons.forEach(btn => btn.remove());
// Add comprehensive PDF styles inline
const styleEl = document.createElement('style');
styleEl.textContent = `
.pdf-content {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
font-size: 12pt;
line-height: 1.6;
color: #24292e;
padding: 20px;
}
.pdf-content h1 {
font-size: 24pt;
font-weight: 600;
margin-top: 0;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #eaecef;
}
.pdf-content h1:first-child {
margin-top: 0;
}
.pdf-content h2 {
font-size: 20pt;
font-weight: 600;
margin-top: 24px;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #eaecef;
}
.pdf-content h3 {
font-size: 16pt;
font-weight: 600;
margin-top: 24px;
margin-bottom: 16px;
}
.pdf-content h4 {
font-size: 14pt;
font-weight: 600;
margin-top: 16px;
margin-bottom: 16px;
}
.pdf-content h5, .pdf-content h6 {
font-size: 12pt;
font-weight: 600;
margin-top: 16px;
margin-bottom: 16px;
}
.pdf-content p {
margin-top: 0;
margin-bottom: 16px;
line-height: 1.4;
}
.pdf-content ul, .pdf-content ol {
list-style: none;
margin-top: 0;
margin-bottom: 16px;
padding-left: 0;
padding-bottom: 8px;
}
.pdf-content li {
display: flex;
align-items: flex-start;
flex-wrap: wrap;
margin-bottom: 8px;
padding-left: 0;
line-height: 1.6;
}
/* Nested lists should break to new line */
.pdf-content li > ul,
.pdf-content li > ol {
width: 100%;
margin-top: 8px;
margin-bottom: 0;
padding-left: 0;
}
/* Custom bullet for ul */
.pdf-content ul > li::before {
content: "•";
display: inline-block;
width: 1.2em;
margin-right: 0.6em;
margin-left: 1.5em;
font-size: 1em;
color: #24292e;
flex-shrink: 0;
line-height: 1.6;
}
/* Custom numbering for ol */
.pdf-content ol {
counter-reset: custom-num;
}
.pdf-content ol > li {
counter-increment: custom-num;
}
.pdf-content ol > li::before {
content: counter(custom-num) ".";
display: inline-block;
width: 1.8em;
margin-right: 0.5em;
margin-left: 1em;
font-size: 1em;
color: #24292e;
flex-shrink: 0;
text-align: right;
line-height: 1.6;
}
/* Nested ul inside ol - bullets with proper indentation */
.pdf-content ol > li > ul > li::before {
content: "•";
margin-left: 3em;
width: 1.2em;
}
/* Nested ul inside ul - circle bullets (level 2) */
.pdf-content ul ul > li::before {
content: "○";
margin-left: 3em;
width: 1.2em;
}
/* Nested ul inside nested ul (inside ol) - circle bullets */
.pdf-content ol > li > ul ul > li::before {
content: "○";
margin-left: 4.5em;
width: 1.2em;
}
/* Deeply nested ul - square bullets (level 3) */
.pdf-content ul ul ul > li::before {
content: "▪";
margin-left: 4.5em;
width: 1.2em;
font-size: 0.8em;
}
.pdf-content table {
border-collapse: collapse;
width: 100%;
margin-bottom: 16px;
}
.pdf-content table th,
.pdf-content table td {
padding: 8px 12px 20px 12px;
border: 1px solid #dfe2e5;
text-align: left;
vertical-align: middle;
line-height: 1.4;
}
.pdf-content table th {
background-color: #f6f8fa;
font-weight: 600;
}
.pdf-content table tr:nth-child(even) {
background-color: #f6f8fa;
}
.pdf-content code {
background-color: #f6f8fa;
padding: 2px 6px;
border-radius: 3px;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-size: 11pt;
}
.pdf-content pre {
background-color: #f6f8fa;
padding: 16px 16px 24px 16px;
border-radius: 6px;
overflow: visible;
margin-bottom: 16px;
page-break-inside: avoid;
min-height: 60px;
}
.pdf-content pre code {
background-color: transparent;
padding: 0 0 8px 0;
font-size: 10pt;
line-height: 1.5;
display: block;
}
.pdf-content blockquote {
padding: 8px 16px 20px 16px;
margin: 0 0 16px 0;
border-left: 4px solid #dfe2e5;
color: #6a737d;
line-height: 1.4;
}
.pdf-content blockquote p {
margin: 0;
line-height: 1.4;
}
.pdf-content hr {
height: 2px;
padding: 0;
margin: 24px 0;
background-color: #e1e4e8;
border: 0;
}
.pdf-content a {
color: #0366d6;
text-decoration: none;
}
.pdf-content strong {
font-weight: 600;
}
.pdf-content img {
max-width: 100%;
height: auto;
}
.pdf-content input[type="checkbox"] {
margin-right: 8px;
}
`;
element.className = 'pdf-content';
// Create wrapper with styles
const wrapper = document.createElement('div');
wrapper.appendChild(styleEl);
wrapper.appendChild(element);
const opt = {
margin: [15, 15, 15, 15],
filename: 'document.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: {
scale: 2,
useCORS: true,
logging: false,
letterRendering: true,
backgroundColor: '#ffffff'
},
jsPDF: {
unit: 'mm',
format: 'a4',
orientation: 'portrait',
compress: true
},
pagebreak: { mode: ['avoid-all', 'css', 'legacy'] }
};
html2pdf().set(opt).from(wrapper).save().then(() => {
setError('');
}).catch((err) => {
setError('Failed to generate PDF');
});
};
return (
<>
{/* Input Section - Always visible */}
Get Started
{/* Tab Navigation */}
{/* Tab Content */}
{(activeTab !== 'create' || !createNewCompleted) && (
{/* Create New Tab Content */}
{activeTab === 'create' && (
<>
{!createNewCompleted ? (
Create New Markdown Document
Choose how you'd like to begin writing
💡 Tip: You can always import markdown later using the URL, Paste, or Open tabs.
) : (
✓ Document ready: {markdownText ? `${stats.words} words, ${stats.characters} characters, ${stats.lines} lines` : 'Empty document'}
)}
>
)}
{/* URL Tab Content */}
{activeTab === 'url' && (
{urlDataSummary && (
✓ Markdown loaded: ({urlDataSummary.size.toLocaleString()} chars, {urlDataSummary.lines} lines)
)}
{!urlDataSummary && (
setFetchUrl(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && !fetching && fetchUrl.trim() && handleFetchFromURL()}
placeholder="https://raw.githubusercontent.com/user/repo/main/README.md"
className="tool-input w-full"
disabled={fetching}
/>
Enter a URL to a markdown file (GitHub raw, Gist, Pastebin, etc.)
)}
)}
{/* Paste Tab Content */}
{activeTab === 'paste' && (
pasteCollapsed ? (
✓ Markdown loaded: {pasteDataSummary.format} ({pasteDataSummary.size.toLocaleString()} chars, {pasteDataSummary.lines} lines)
) : (
{error && (
Error: {error}
)}
Paste markdown text
)
)}
{/* Open Tab Content */}
{activeTab === 'open' && (
fileDataSummary ? (
✓ File loaded: {fileDataSummary.format} ({fileDataSummary.size.toLocaleString()} chars, {fileDataSummary.lines} lines) - {fileDataSummary.filename}
) : (
🔒 Privacy: Your data stays in your browser. We don't store or upload anything.
)
)}
)}
{/* Input Method Change Confirmation Modal */}
{showInputChangeModal && (
)}
{/* Editor Section */}
{(activeTab !== 'create' || createNewCompleted) && (
{/* Editor Header - Sticky */}
Markdown Editor
{/* Statistics */}
{stats.words} words
•
{stats.characters} chars
•
{stats.lines} lines
•
{stats.readingTime} min read
{/* View Mode Controls */}
{/* Hide split button on mobile (< lg) */}
{/* Markdown Toolbar */}
{(viewMode === 'editor' || viewMode === 'split') && (
{toolbarButtons.map((btn, idx) => {
const Icon = btn.icon;
const prevGroup = idx > 0 ? toolbarButtons[idx - 1].group : null;
const showSeparator = idx > 0 && btn.group !== prevGroup;
return (
{/* Group Separator */}
{showSeparator && (
)}
{/* Tooltip */}
{btn.label}
{/* Heading Dropdown */}
{btn.isDropdown && showHeadingDropdown && (
{[1, 2, 3, 4, 5, 6].map(level => (
))}
)}
);
})}
)}
{/* Editor Content */}
{/* Markdown Editor */}
{(viewMode === 'editor' || viewMode === 'split') && (
)}
{/* Preview */}
{(viewMode === 'preview' || viewMode === 'split') && (
{markdownText ? (
) : (
Preview will appear here
)}
)}
)}
{/* Export Section */}
{markdownText && (
Export Options
Download your markdown in different formats
{/* Export as Markdown */}
{/* Export as PDF */}
{/* Export as Full HTML */}
{/* Export as HTML Content Only */}
{/* Export as Plain Text */}
{/* Copy to Clipboard */}
{/* Export Info */}
Export Information
- Markdown: Original markdown with all formatting (.md file)
- PDF: Professional PDF document with formatted content - perfect for sharing and printing!
- Full HTML: Complete standalone HTML page with GitHub Dark theme, syntax highlighting (Highlight.js), and working copy buttons - ready to use!
- HTML Content: Body content only, ready to paste into web pages
- Plain Text: Markdown-ready text file, keeps all syntax (.txt file)
- Copy: Copy markdown to clipboard for pasting
)}
{/* Usage Tips */}
setUsageTipsExpanded(!usageTipsExpanded)}
className="px-4 py-3 cursor-pointer hover:bg-blue-100 dark:hover:bg-blue-900/30 transition-colors flex items-center justify-between"
>
💡 Usage Tips
{usageTipsExpanded ? : }
{usageTipsExpanded && (
{/* Input Methods */}
📝 Input Methods:
- Create New: Start with empty editor or load sample markdown
- URL: Fetch markdown from GitHub, Gist, or any public URL
- Paste: Paste markdown, HTML (auto-converts), or plain text
- Open: Load .md, .txt, or .html files from your computer
{/* Editing Features */}
✏️ Editing Features
- Live Preview: See your markdown rendered in real-time
- Syntax Highlighting: Code blocks with automatic language detection
- View Modes: Switch between Split, Editor Only, Preview Only, or Fullscreen
- Toolbar: Quick formatting buttons for headers, bold, italic, links, code, and more
- Statistics: Track word count, character count, lines, and reading time
- GitHub Flavored Markdown: Full GFM support including tables and task lists
{/* Markdown Syntax */}
📖 Markdown Syntax
- Headers: # H1, ## H2, ### H3, etc.
- Bold: **bold text** or __bold text__
- Italic: *italic text* or _italic text_
- Underline: __underlined text__ (custom syntax)
- Code: `inline code` or ```language for code blocks
- Links: [text](url)
- Images: 
- Lists: - or * for unordered, 1. for ordered
- Tables: {'|'} Header {'|'} Header {'|'} with {'|'} --- {'|'} --- {'|'} separator
- Blockquotes: {'>'} quoted text
{/* Export Options */}
📤 Export Options
- Markdown (.md): Standard markdown format
- Full HTML: Standalone page with styling and working copy buttons
- HTML Content: Just the body content for embedding
- Plain Text (.txt): Markdown-ready text file
- Copy: Copy to clipboard for quick sharing
{/* Data Privacy */}
💾 Data Privacy
- 100% Client-Side: All processing happens in your browser
- No Server Upload: Your markdown never leaves your device
- No Tracking: We don't store or track your content
- Privacy-First: Safe for confidential documents
)}
{/* Related Tools */}
>
);
};
// Input Method Change Confirmation Modal Component
const InputChangeConfirmationModal = ({ markdownText, stats, currentMethod, newMethod, onConfirm, onCancel }) => {
const getMethodName = (method) => {
switch (method) {
case 'create': return 'Create New';
case 'create_empty': return 'Start Empty';
case 'create_sample': return 'Load Sample';
case 'url': return 'URL Import';
case 'paste': return 'Paste Data';
case 'open': return 'File Upload';
default: return method;
}
};
return (
{/* Header */}
Change Input Method
This will clear all current markdown
{/* Content */}
{(newMethod === 'create_empty' || newMethod === 'create_sample') ? (
<>Using {getMethodName(newMethod)} will clear all current markdown.>
) : (
<>Switching from {getMethodName(currentMethod)} to {getMethodName(newMethod)} will clear all current markdown.>
)}
This will permanently delete:
- • {stats.words} words of markdown content
- • {stats.characters} characters
- • {stats.lines} lines
Tip: Consider downloading your current markdown before switching methods to avoid losing your work.
{/* Footer */}
);
};
export default MarkdownEditor;