Files
dewedev/src/pages/MarkdownEditor.js

2268 lines
84 KiB
JavaScript
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 `
<div class="code-block-wrapper">
<div class="code-block-header">
<span class="code-block-language">${displayLang}</span>
<button
class="code-block-copy"
data-code-id="${blockId}"
title="Copy code"
>
Copy
</button>
</div>
<pre><code id="${blockId}" class="hljs language-${displayLang}">${highlightedCode}</code></pre>
</div>
`;
};
marked.setOptions({
gfm: true,
breaks: true,
renderer: renderer
});
// Enable GFM extensions including task lists
marked.use({
gfm: true,
breaks: true
});
// Enable emoji support
marked.use(markedEmoji({
emojis: {}, // Uses default emoji set
unicode: true // Use unicode emojis
}));
}, []);
// Parse markdown to HTML with custom underline support
const parseMarkdown = (markdown) => {
try {
// Convert __text__ to <u>text</u> before parsing (custom underline syntax)
// But preserve __ at start of line (which is for bold in some contexts)
let processed = markdown.replace(/(?<!_)__(?!_)(.+?)__(?!_)/g, '<u>$1</u>');
const html = marked.parse(processed || '');
return DOMPurify.sanitize(html, {
ADD_TAGS: ['u', 'button', 'input'],
ADD_ATTR: ['data-code-id', 'title', 'id', 'type', 'checked', 'disabled']
});
} catch (e) {
return '<p>Error parsing markdown</p>';
}
};
// 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
<?php
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
echo "Hello, World!";
?>
\`\`\`
## 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(/<body[^>]*>([\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(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, '# $1\n\n');
markdown = markdown.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, '## $1\n\n');
markdown = markdown.replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, '### $1\n\n');
markdown = markdown.replace(/<h4[^>]*>([\s\S]*?)<\/h4>/gi, '#### $1\n\n');
markdown = markdown.replace(/<h5[^>]*>([\s\S]*?)<\/h5>/gi, '##### $1\n\n');
markdown = markdown.replace(/<h6[^>]*>([\s\S]*?)<\/h6>/gi, '###### $1\n\n');
// Bold and Italic
markdown = markdown.replace(/<strong[^>]*>([\s\S]*?)<\/strong>/gi, '**$1**');
markdown = markdown.replace(/<b[^>]*>([\s\S]*?)<\/b>/gi, '**$1**');
markdown = markdown.replace(/<em[^>]*>([\s\S]*?)<\/em>/gi, '*$1*');
markdown = markdown.replace(/<i[^>]*>([\s\S]*?)<\/i>/gi, '*$1*');
markdown = markdown.replace(/<u[^>]*>([\s\S]*?)<\/u>/gi, '__$1__');
// Links
markdown = markdown.replace(/<a[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, '[$2]($1)');
// Images
markdown = markdown.replace(/<img[^>]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*>/gi, '![$2]($1)');
markdown = markdown.replace(/<img[^>]*alt="([^"]*)"[^>]*src="([^"]*)"[^>]*>/gi, '![$1]($2)');
// Inline code (must come after code blocks)
markdown = markdown.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, '`$1`');
// Tables
markdown = markdown.replace(/<table[^>]*>([\s\S]*?)<\/table>/gi, (match, content) => {
let tableMarkdown = '\n';
const rows = content.match(/<tr[^>]*>([\s\S]*?)<\/tr>/gi) || [];
rows.forEach((row, index) => {
const cells = row.match(/<t[hd][^>]*>([\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(/<li[^>]*>([\s\S]*?)<\/li>/gi, (match, content) => {
content = content.trim();
return '- ' + content + '\n';
});
markdown = markdown.replace(/<ul[^>]*>([\s\S]*?)<\/ul>/gi, '$1\n');
markdown = markdown.replace(/<ol[^>]*>([\s\S]*?)<\/ol>/gi, '$1\n');
// Blockquotes
markdown = markdown.replace(/<blockquote[^>]*>([\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(/<p[^>]*>([\s\S]*?)<\/p>/gi, (match, content) => {
content = content.trim();
return content + '\n\n';
});
markdown = markdown.replace(/<br\s*\/?>/gi, '\n');
markdown = markdown.replace(/<hr\s*\/?>/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(/&lt;/g, '<');
markdown = markdown.replace(/&gt;/g, '>');
markdown = markdown.replace(/&amp;/g, '&');
markdown = markdown.replace(/&quot;/g, '"');
markdown = markdown.replace(/&#39;/g, "'");
markdown = markdown.replace(/&nbsp;/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 = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Markdown Document</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.2.0/github-markdown.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<style>
body {
max-width: 800px;
margin: 40px auto;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
background: #0d1117;
color: #c9d1d9;
}
.markdown-body {
box-sizing: border-box;
background: #0d1117;
color: #c9d1d9;
}
/* Code block styling with copy button */
.markdown-body pre {
position: relative;
background: #161b22;
border: 1px solid #30363d;
border-radius: 6px;
padding: 16px;
overflow: auto;
}
.markdown-body pre code {
background: transparent;
padding: 0;
border: none;
}
.code-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: #161b22;
border: 1px solid #30363d;
border-bottom: none;
border-radius: 6px 6px 0 0;
margin-bottom: -1px;
}
.code-language {
font-size: 12px;
color: #8b949e;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
}
.copy-button {
background: #21262d;
border: 1px solid #30363d;
color: #c9d1d9;
padding: 4px 12px;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.copy-button:hover {
background: #30363d;
border-color: #8b949e;
}
.copy-button:active {
background: #161b22;
}
.copy-button.copied {
color: #3fb950;
border-color: #3fb950;
}
/* Hide inline copy buttons that appear after language name */
.markdown-body pre > code::after,
.markdown-body pre::after,
.markdown-body code + button,
.code-block-copy {
display: none !important;
}
</style>
</head>
<body class="markdown-body">
${html}
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>
// Apply syntax highlighting
document.addEventListener('DOMContentLoaded', function() {
// Remove old code-block-header and code-block-wrapper elements
document.querySelectorAll('.code-block-header').forEach(el => el.remove());
document.querySelectorAll('.code-block-wrapper').forEach(wrapper => {
// Move children out of wrapper before removing it
while (wrapper.firstChild) {
wrapper.parentNode.insertBefore(wrapper.firstChild, wrapper);
}
wrapper.remove();
});
// Remove any inline copy buttons or text that might appear
document.querySelectorAll('.code-block-copy, button[data-code-id]').forEach(el => el.remove());
// Highlight all code blocks
document.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
// Add copy button
const pre = block.parentElement;
const language = block.className.match(/language-(\\w+)/)?.[1] || 'text';
// Remove any text nodes that might contain "Copy" after the language
const parent = pre.parentNode;
Array.from(parent.childNodes).forEach(node => {
if (node.nodeType === Node.TEXT_NODE && node.textContent.includes('Copy')) {
node.remove();
}
});
// Create header with language and copy button
const header = document.createElement('div');
header.className = 'code-header';
const langLabel = document.createElement('span');
langLabel.className = 'code-language';
langLabel.textContent = language;
const copyBtn = document.createElement('button');
copyBtn.className = 'copy-button';
copyBtn.textContent = 'Copy';
copyBtn.onclick = function() {
navigator.clipboard.writeText(block.textContent).then(() => {
copyBtn.textContent = 'Copied!';
copyBtn.classList.add('copied');
setTimeout(() => {
copyBtn.textContent = 'Copy';
copyBtn.classList.remove('copied');
}, 2000);
});
};
header.appendChild(langLabel);
header.appendChild(copyBtn);
// Insert header before pre
pre.parentNode.insertBefore(header, pre);
// Adjust pre border radius
pre.style.borderRadius = '0 0 6px 6px';
});
});
</script>
</body>
</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 (
<>
<SEO
title="Free Markdown Editor with Live Preview & PDF Export"
description="✓ Live preview ✓ GitHub Flavored Markdown ✓ Export to PDF/HTML ✓ Syntax highlighting ✓ 100% free. Start writing now - no signup required!"
keywords="markdown editor, markdown preview, markdown to html, markdown to pdf, markdown converter, online markdown, github markdown, gfm, markdown syntax, code editor, documentation tool, readme editor"
path="/markdown-editor"
toolId="markdown-editor"
/>
<ToolLayout
title="Markdown Editor"
description="Write and preview markdown with live rendering, syntax highlighting, and export options"
icon={FileText}
>
{/* Input Section - Always visible */}
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mb-6">
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Get Started</h3>
</div>
{/* Tab Navigation */}
<div className="border-b border-gray-200 dark:border-gray-700">
<div className="flex">
<button
onClick={() => handleTabChange('create')}
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === 'create'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
: 'text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
<Plus className="h-4 w-4 flex-shrink-0" />
Create New
</button>
<button
onClick={() => handleTabChange('url')}
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === 'url'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
: 'text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
<Globe className="h-4 w-4 flex-shrink-0" />
URL
</button>
<button
onClick={() => handleTabChange('paste')}
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === 'paste'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
: 'text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
<FileText className="h-4 w-4 flex-shrink-0" />
Paste
</button>
<button
onClick={() => handleTabChange('open')}
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === 'open'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
: 'text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
<Upload className="h-4 w-4 flex-shrink-0" />
Open
</button>
</div>
</div>
{/* Tab Content */}
{(activeTab !== 'create' || !createNewCompleted) && (
<div className="p-3 sm:p-4">
{/* Create New Tab Content */}
{activeTab === 'create' && (
<>
{!createNewCompleted ? (
<div className="space-y-4">
<div className="text-center">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
Create New Markdown Document
</h3>
<p className="text-sm text-gray-600 dark:text-gray-600 mb-4">
Choose how you'd like to begin writing
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<button
onClick={() => {
if (hasModifiedData()) {
setPendingTabChange('create_empty');
setShowInputChangeModal(true);
} else {
clearAllData();
setMarkdownText('');
setCreateNewCompleted(true);
}
}}
className="flex flex-col items-center p-6 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg hover:border-blue-500 dark:hover:border-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors group"
>
<Plus className="h-8 w-8 text-gray-600 group-hover:text-blue-500 dark:group-hover:text-blue-400 mb-2" />
<span className="font-medium text-gray-900 dark:text-gray-100 group-hover:text-blue-600 dark:group-hover:text-blue-400">
Start Empty
</span>
<span className="text-xs text-gray-600 dark:text-gray-600 text-center mt-1">
Begin with a blank markdown document
</span>
</button>
<div className="flex flex-col gap-3 flex-1">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
Or choose a template:
</label>
<select
value={selectedTemplate}
onChange={(e) => setSelectedTemplate(e.target.value)}
className="tool-input"
>
<option value="sample">Sample - Markdown Features</option>
<option value="readme">README - Project Documentation</option>
<option value="blog">Blog Post - Article Template</option>
<option value="documentation">API Documentation</option>
<option value="meeting">Meeting Notes</option>
<option value="changelog">Changelog - Version History</option>
</select>
<button
onClick={() => {
if (hasModifiedData()) {
setPendingTabChange('create_sample');
setShowInputChangeModal(true);
} else {
clearAllData();
setMarkdownText(templates[selectedTemplate]);
setCreateNewCompleted(true);
}
}}
className="flex flex-col items-center p-6 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg hover:border-green-500 dark:hover:border-green-400 hover:bg-green-50 dark:hover:bg-green-900/20 transition-colors group"
>
<FileText className="h-8 w-8 text-gray-600 group-hover:text-green-500 dark:group-hover:text-green-400 mb-2" />
<span className="font-medium text-gray-900 dark:text-gray-100 group-hover:text-green-600 dark:group-hover:text-green-400">
Load Template
</span>
<span className="text-xs text-gray-600 dark:text-gray-600 text-center mt-1">
Start with a pre-made template
</span>
</button>
</div>
</div>
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-3">
<p className="text-xs text-blue-700 dark:text-blue-300">
💡 <strong>Tip:</strong> You can always import markdown later using the URL, Paste, or Open tabs.
</p>
</div>
</div>
) : (
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<span className="text-sm text-green-700 dark:text-green-300 break-words">
✓ Document ready: {markdownText ? `${stats.words} words, ${stats.characters} characters, ${stats.lines} lines` : 'Empty document'}
</span>
<button
onClick={() => setCreateNewCompleted(false)}
className="text-sm text-blue-600 dark:text-blue-400 hover:underline whitespace-nowrap"
>
Change Option ▼
</button>
</div>
</div>
)}
</>
)}
{/* URL Tab Content */}
{activeTab === 'url' && (
<div className="p-4">
{urlDataSummary && (
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<span className="text-sm text-green-700 dark:text-green-300 break-words">
✓ Markdown loaded: ({urlDataSummary.size.toLocaleString()} chars, {urlDataSummary.lines} lines)
</span>
<button
onClick={() => setUrlDataSummary(null)}
className="text-sm text-blue-600 dark:text-blue-400 hover:underline whitespace-nowrap"
>
Fetch New URL ▼
</button>
</div>
</div>
)}
{!urlDataSummary && (
<div className="space-y-3">
<div className="flex gap-2">
<div className="relative flex-1">
<input
type="url"
value={fetchUrl}
onChange={(e) => 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}
/>
</div>
<button
onClick={handleFetchFromURL}
disabled={fetching || !fetchUrl.trim()}
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-medium px-4 py-2 rounded-md transition-colors flex items-center whitespace-nowrap"
>
{fetching ? 'Fetching...' : 'Fetch Data'}
</button>
</div>
<p className="text-xs text-gray-600 dark:text-gray-600">
Enter a URL to a markdown file (GitHub raw, Gist, Pastebin, etc.)
</p>
</div>
)}
</div>
)}
{/* Paste Tab Content */}
{activeTab === 'paste' && (
pasteCollapsed ? (
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<span className="text-sm text-green-700 dark:text-green-300 break-words">
✓ Markdown loaded: {pasteDataSummary.format} ({pasteDataSummary.size.toLocaleString()} chars, {pasteDataSummary.lines} lines)
</span>
<button
onClick={() => setPasteCollapsed(false)}
className="text-sm text-blue-600 dark:text-blue-400 hover:underline whitespace-nowrap"
>
Edit Input ▼
</button>
</div>
</div>
) : (
<div className="space-y-3">
<div>
<CodeMirrorEditor
value={inputText}
onChange={setInputText}
language="markdown"
placeholder="Paste your markdown here..."
maxLines={12}
showToggle={true}
className="w-full"
/>
</div>
{error && (
<div className="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<p className="text-sm text-red-600 dark:text-red-400">
<strong>Error:</strong> {error}
</p>
</div>
)}
<div className="flex items-center justify-between flex-shrink-0">
<div className="text-sm text-gray-600 dark:text-gray-600">
Paste markdown text
</div>
<button
onClick={handleParseMarkdown}
disabled={!inputText.trim()}
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed text-white font-medium px-4 py-2 rounded-md transition-colors flex-shrink-0"
>
Load Markdown
</button>
</div>
</div>
)
)}
{/* Open Tab Content */}
{activeTab === 'open' && (
fileDataSummary ? (
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<span className="text-sm text-green-700 dark:text-green-300 break-words">
✓ File loaded: {fileDataSummary.format} ({fileDataSummary.size.toLocaleString()} chars, {fileDataSummary.lines} lines) - {fileDataSummary.filename}
</span>
<button
onClick={() => setFileDataSummary(null)}
className="text-sm text-blue-600 dark:text-blue-400 hover:underline whitespace-nowrap"
>
Upload New File ▼
</button>
</div>
</div>
) : (
<div className="space-y-3">
<input
ref={fileInputRef}
type="file"
accept=".md,.markdown,.txt,.html,.htm"
onChange={handleFileUpload}
className="tool-input"
/>
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-md p-3">
<p className="text-xs text-green-700 dark:text-green-300">
🔒 <strong>Privacy:</strong> Your data stays in your browser. We don't store or upload anything.
</p>
</div>
</div>
)
)}
</div>
)}
</div>
{/* Input Method Change Confirmation Modal */}
{showInputChangeModal && (
<InputChangeConfirmationModal
markdownText={markdownText}
stats={stats}
currentMethod={activeTab}
newMethod={pendingTabChange}
onConfirm={confirmInputChange}
onCancel={cancelInputChange}
/>
)}
{/* Editor Section */}
{(activeTab !== 'create' || createNewCompleted) && (
<div className={`bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden min-w-0 w-full max-w-full ${isFullscreen ? 'fixed inset-0 z-[9999] flex flex-col !mt-0' : 'mb-6'}`}>
{/* Editor Header - Sticky */}
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700 sticky top-0 bg-white dark:bg-gray-800 z-10">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Edit3 className="h-5 w-5 text-blue-600 dark:text-blue-400" />
Markdown Editor
</h3>
{/* Statistics */}
<div className="hidden sm:flex items-center gap-3 text-xs text-gray-600 dark:text-gray-600">
<span>{stats.words} words</span>
<span></span>
<span>{stats.characters} chars</span>
<span></span>
<span>{stats.lines} lines</span>
<span></span>
<span>{stats.readingTime} min read</span>
</div>
</div>
{/* View Mode Controls */}
<div className="flex items-center gap-2">
<div className="flex border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<button
onClick={() => setViewMode('editor')}
className={`flex items-center gap-2 px-3 py-2 text-sm font-medium transition-colors ${
viewMode === 'editor'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
: 'text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
title="Editor Only"
>
<Type className="h-4 w-4" />
<span className="hidden sm:inline">Editor</span>
</button>
{/* Hide split button on mobile (< lg) */}
<button
onClick={() => setViewMode('split')}
className={`hidden lg:flex items-center gap-2 px-3 py-2 text-sm font-medium transition-colors border-l border-gray-200 dark:border-gray-700 ${
viewMode === 'split'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
: 'text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
title="Split View"
>
<Columns className="h-4 w-4" />
<span className="hidden sm:inline">Split</span>
</button>
<button
onClick={() => setViewMode('preview')}
className={`flex items-center gap-2 px-3 py-2 text-sm font-medium transition-colors border-l border-gray-200 dark:border-gray-700 ${
viewMode === 'preview'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
: 'text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
title="Preview Only"
>
<Eye className="h-4 w-4" />
<span className="hidden sm:inline">Preview</span>
</button>
</div>
<button
onClick={() => setIsFullscreen(!isFullscreen)}
className="p-2 text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
title={isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'}
>
{isFullscreen ? <Minimize2 className="h-4 w-4" /> : <Maximize2 className="h-4 w-4" />}
</button>
</div>
</div>
</div>
{/* Markdown Toolbar */}
{(viewMode === 'editor' || viewMode === 'split') && (
<div className="px-4 py-2 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/50 min-w-0 w-full">
<div className="flex items-center gap-1 overflow-x-auto relative" style={{ WebkitOverflowScrolling: 'touch' }}>
{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 (
<React.Fragment key={idx}>
{/* Group Separator */}
{showSeparator && (
<div className="h-6 w-px bg-gray-300 dark:bg-gray-600 mx-1" />
)}
<div className="relative group">
<button
onClick={btn.action}
className="p-2 text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-700 rounded transition-colors"
aria-label={btn.label}
>
<Icon className="h-4 w-4" />
</button>
{/* Tooltip */}
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded whitespace-nowrap opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 pointer-events-none z-[60]">
{btn.label}
</div>
{/* Heading Dropdown */}
{btn.isDropdown && showHeadingDropdown && (
<div className="absolute top-full left-0 mt-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg py-1 z-50 min-w-[120px]">
{[1, 2, 3, 4, 5, 6].map(level => (
<button
key={level}
onClick={() => insertHeading(level)}
className="w-full px-3 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 text-sm"
>
<span className="font-semibold">H{level}</span> - {'#'.repeat(level)} Heading {level}
</button>
))}
</div>
)}
</div>
</React.Fragment>
);
})}
</div>
</div>
)}
{/* Editor Content */}
<div className={`${viewMode === 'split' ? 'grid grid-cols-1 lg:grid-cols-2' : ''} overflow-hidden min-w-0 w-full`}>
{/* Markdown Editor */}
{(viewMode === 'editor' || viewMode === 'split') && (
<div className={`${viewMode === 'split' ? 'border-r border-gray-200 dark:border-gray-700' : ''} ${isFullscreen ? 'h-[calc(100vh-120px)]' : 'h-[600px]'} w-full overflow-hidden min-w-0`}>
<div className="h-full">
<CodeMirrorEditor
value={markdownText}
onChange={setMarkdownText}
language="markdown"
placeholder="# Start writing your markdown here...
You can use:
- **Bold** and *italic* text
- Lists and checkboxes
- Code blocks with syntax highlighting
- Tables, links, and images
- And much more!"
showToggle={false}
maxLines={999}
height={isFullscreen ? 'calc(100vh - 120px)' : '600px'}
/>
</div>
</div>
)}
{/* Preview */}
{(viewMode === 'preview' || viewMode === 'split') && (
<div className={`flex flex-col bg-gray-50 dark:bg-gray-900 ${isFullscreen ? 'h-[calc(100vh-120px)]' : 'h-[600px]'} w-full overflow-hidden min-w-0`}>
<div className="flex-1 overflow-auto p-4 min-w-0 w-full">
{markdownText ? (
<div
className="markdown-preview prose prose-slate dark:prose-invert max-w-full"
dangerouslySetInnerHTML={{ __html: parseMarkdown(markdownText) }}
/>
) : (
<div className="flex items-center justify-center h-full text-gray-600 dark:text-gray-600">
<div className="text-center">
<EyeOff className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p className="text-sm">Preview will appear here</p>
</div>
</div>
)}
</div>
</div>
)}
</div>
</div>
)}
{/* Export Section */}
{markdownText && (
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden min-w-0 w-full max-w-full">
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Download className="h-5 w-5 text-blue-600 dark:text-blue-400" />
Export Options
</h3>
<p className="text-sm text-gray-600 dark:text-gray-600 mt-1">
Download your markdown in different formats
</p>
</div>
<div className="p-4 sm:p-6">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-4">
{/* Export as Markdown */}
<button
onClick={handleExportMarkdown}
className="flex flex-col items-center justify-center p-4 md:p-6 border-2 border-gray-200 dark:border-gray-700 rounded-lg hover:border-blue-500 dark:hover:border-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-all group relative"
>
<FileText className="h-8 w-8 absolute left-4 top-5 md:static md:left-0 md:top-0 text-gray-600 dark:text-gray-600 group-hover:text-blue-600 dark:group-hover:text-blue-400 mb-3" />
<span className="font-medium text-gray-900 dark:text-white mb-1">Markdown</span>
<span className="text-xs text-gray-600 dark:text-gray-600">.md file</span>
</button>
{/* Export as PDF */}
<button
onClick={handleExportPDF}
className="flex flex-col items-center justify-center p-4 md:p-6 border-2 border-gray-200 dark:border-gray-700 rounded-lg hover:border-red-500 dark:hover:border-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-all group relative"
>
<FileDown className="h-8 w-8 absolute left-4 top-5 md:static md:left-0 md:top-0 text-gray-600 dark:text-gray-600 group-hover:text-red-600 dark:group-hover:text-red-400 mb-3" />
<span className="font-medium text-gray-900 dark:text-white mb-1">PDF</span>
<span className="text-xs text-gray-600 dark:text-gray-600">.pdf file</span>
</button>
{/* Export as Full HTML */}
<button
onClick={handleExportHTML}
className="flex flex-col items-center justify-center p-4 md:p-6 border-2 border-gray-200 dark:border-gray-700 rounded-lg hover:border-green-500 dark:hover:border-green-400 hover:bg-green-50 dark:hover:bg-green-900/20 transition-all group relative"
>
<Globe className="h-8 w-8 absolute left-4 top-5 md:static md:left-0 md:top-0 text-gray-600 dark:text-gray-600 group-hover:text-green-600 dark:group-hover:text-green-400 mb-3" />
<span className="font-medium text-gray-900 dark:text-white mb-1">Full HTML</span>
<span className="text-xs text-gray-600 dark:text-gray-600">.html page</span>
</button>
{/* Export as HTML Content Only */}
<button
onClick={handleExportHTMLContent}
className="flex flex-col items-center justify-center p-4 md:p-6 border-2 border-gray-200 dark:border-gray-700 rounded-lg hover:border-teal-500 dark:hover:border-teal-400 hover:bg-teal-50 dark:hover:bg-teal-900/20 transition-all group relative"
>
<Code className="h-8 w-8 absolute left-4 top-5 md:static md:left-0 md:top-0 text-gray-600 dark:text-gray-600 group-hover:text-teal-600 dark:group-hover:text-teal-400 mb-3" />
<span className="font-medium text-gray-900 dark:text-white mb-1">HTML Content</span>
<span className="text-xs text-gray-600 dark:text-gray-600">Body only</span>
</button>
{/* Export as Plain Text */}
<button
onClick={handleExportPlainText}
className="flex flex-col items-center justify-center p-4 md:p-6 border-2 border-gray-200 dark:border-gray-700 rounded-lg hover:border-purple-500 dark:hover:border-purple-400 hover:bg-purple-50 dark:hover:bg-purple-900/20 transition-all group relative"
>
<Type className="h-8 w-8 absolute left-4 top-5 md:static md:left-0 md:top-0 text-gray-600 dark:text-gray-600 group-hover:text-purple-600 dark:group-hover:text-purple-400 mb-3" />
<span className="font-medium text-gray-900 dark:text-white mb-1">Plain Text</span>
<span className="text-xs text-gray-600 dark:text-gray-600">.txt file</span>
</button>
{/* Copy to Clipboard */}
<button
onClick={handleCopyToClipboard}
className="flex flex-col items-center justify-center p-4 md:p-6 border-2 border-gray-200 dark:border-gray-700 rounded-lg hover:border-orange-500 dark:hover:border-orange-400 hover:bg-orange-50 dark:hover:bg-orange-900/20 transition-all group relative"
>
<Download className="h-8 w-8 absolute left-4 top-5 md:static md:left-0 md:top-0 text-gray-600 dark:text-gray-600 group-hover:text-orange-600 dark:group-hover:text-orange-400 mb-3" />
<span className="font-medium text-gray-900 dark:text-white mb-1">Copy</span>
<span className="text-xs text-gray-600 dark:text-gray-600">To clipboard</span>
</button>
</div>
{/* Export Info */}
<div className="mt-6 p-4 bg-amber-50 dark:bg-amber-900/20 rounded-lg border border-amber-200 dark:border-amber-800">
<div className="flex items-start gap-3">
<AlertTriangle className="h-5 w-5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" />
<div className="text-sm text-amber-800 dark:text-amber-200">
<p className="font-medium mb-1">Export Information</p>
<ul className="list-disc list-inside space-y-1 text-amber-700 dark:text-amber-300">
<li><strong>Markdown:</strong> Original markdown with all formatting (.md file)</li>
<li><strong>PDF:</strong> Professional PDF document with formatted content - perfect for sharing and printing!</li>
<li><strong>Full HTML:</strong> Complete standalone HTML page with GitHub Dark theme, syntax highlighting (Highlight.js), and working copy buttons - ready to use!</li>
<li><strong>HTML Content:</strong> Body content only, ready to paste into web pages</li>
<li><strong>Plain Text:</strong> Markdown-ready text file, keeps all syntax (.txt file)</li>
<li><strong>Copy:</strong> Copy markdown to clipboard for pasting</li>
</ul>
</div>
</div>
</div>
</div>
</div>
)}
{/* Usage Tips */}
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md overflow-hidden mt-6">
<div
onClick={() => 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"
>
<h4 className="text-blue-800 dark:text-blue-200 font-medium flex items-center gap-2">
💡 Usage Tips
</h4>
{usageTipsExpanded ? <ChevronUp className="h-4 w-4 text-blue-600 dark:text-blue-400" /> : <ChevronDown className="h-4 w-4 text-blue-600 dark:text-blue-400" />}
</div>
{usageTipsExpanded && (
<div className="px-4 pb-4 text-blue-700 dark:text-blue-300 text-sm space-y-2">
{/* Input Methods */}
<div>
<p className="font-medium mb-1">📝 Input Methods:</p>
<ul className="list-disc list-inside space-y-1 ml-2">
<li><strong>Create New:</strong> Start with empty editor or load sample markdown</li>
<li><strong>URL:</strong> Fetch markdown from GitHub, Gist, or any public URL</li>
<li><strong>Paste:</strong> Paste markdown, HTML (auto-converts), or plain text</li>
<li><strong>Open:</strong> Load .md, .txt, or .html files from your computer</li>
</ul>
</div>
{/* Editing Features */}
<div>
<h4 className="font-semibold text-blue-700 dark:text-blue-300 mb-2"> Editing Features</h4>
<ul className="list-disc list-inside space-y-1 ml-2">
<li><strong>Live Preview:</strong> See your markdown rendered in real-time</li>
<li><strong>Syntax Highlighting:</strong> Code blocks with automatic language detection</li>
<li><strong>View Modes:</strong> Switch between Split, Editor Only, Preview Only, or Fullscreen</li>
<li><strong>Toolbar:</strong> Quick formatting buttons for headers, bold, italic, links, code, and more</li>
<li><strong>Statistics:</strong> Track word count, character count, lines, and reading time</li>
<li><strong>GitHub Flavored Markdown:</strong> Full GFM support including tables and task lists</li>
</ul>
</div>
{/* Markdown Syntax */}
<div>
<h4 className="font-semibold text-blue-700 dark:text-blue-300 mb-2">📖 Markdown Syntax</h4>
<ul className="list-disc list-inside space-y-1 ml-2">
<li><strong>Headers:</strong> # H1, ## H2, ### H3, etc.</li>
<li><strong>Bold:</strong> **bold text** or __bold text__</li>
<li><strong>Italic:</strong> *italic text* or _italic text_</li>
<li><strong>Underline:</strong> __underlined text__ (custom syntax)</li>
<li><strong>Code:</strong> `inline code` or ```language for code blocks</li>
<li><strong>Links:</strong> [text](url)</li>
<li><strong>Images:</strong> ![alt text](url)</li>
<li><strong>Lists:</strong> - or * for unordered, 1. for ordered</li>
<li><strong>Tables:</strong> {'|'} Header {'|'} Header {'|'} with {'|'} --- {'|'} --- {'|'} separator</li>
<li><strong>Blockquotes:</strong> {'>'} quoted text</li>
</ul>
</div>
{/* Export Options */}
<div>
<h4 className="font-semibold text-blue-700 dark:text-blue-300 mb-2">📤 Export Options</h4>
<ul className="list-disc list-inside space-y-1 ml-2">
<li><strong>Markdown (.md):</strong> Standard markdown format</li>
<li><strong>Full HTML:</strong> Standalone page with styling and working copy buttons</li>
<li><strong>HTML Content:</strong> Just the body content for embedding</li>
<li><strong>Plain Text (.txt):</strong> Markdown-ready text file</li>
<li><strong>Copy:</strong> Copy to clipboard for quick sharing</li>
</ul>
</div>
{/* Data Privacy */}
<div>
<h4 className="font-semibold text-blue-700 dark:text-blue-300 mb-2">💾 Data Privacy</h4>
<ul className="list-disc list-inside space-y-1 ml-2">
<li><strong>100% Client-Side:</strong> All processing happens in your browser</li>
<li><strong>No Server Upload:</strong> Your markdown never leaves your device</li>
<li><strong>No Tracking:</strong> We don't store or track your content</li>
<li><strong>Privacy-First:</strong> Safe for confidential documents</li>
</ul>
</div>
</div>
)}
</div>
{/* Related Tools */}
<RelatedTools toolId="markdown-editor" />
</ToolLayout>
</>
);
};
// 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 (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full overflow-hidden">
{/* Header */}
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700 bg-amber-50 dark:bg-amber-900/20">
<div className="flex items-center gap-3">
<div className="flex-shrink-0">
<AlertTriangle className="h-6 w-6 text-amber-600 dark:text-amber-400" />
</div>
<div>
<h3 className="text-lg font-semibold text-amber-900 dark:text-amber-100">
Change Input Method
</h3>
<p className="text-sm text-amber-700 dark:text-amber-300">
This will clear all current markdown
</p>
</div>
</div>
</div>
{/* Content */}
<div className="px-6 py-4">
<p className="text-gray-700 dark:text-gray-300 mb-4">
{(newMethod === 'create_empty' || newMethod === 'create_sample') ? (
<>Using <strong>{getMethodName(newMethod)}</strong> will clear all current markdown.</>
) : (
<>Switching from <strong>{getMethodName(currentMethod)}</strong> to <strong>{getMethodName(newMethod)}</strong> will clear all current markdown.</>
)}
</p>
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-4 mb-4">
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">
This will permanently delete:
</h4>
<ul className="text-sm text-gray-600 dark:text-gray-600 space-y-1">
<li> {stats.words} words of markdown content</li>
<li> {stats.characters} characters</li>
<li> {stats.lines} lines</li>
</ul>
</div>
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-3">
<div className="flex items-start gap-2">
<AlertTriangle className="h-4 w-4 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
<p className="text-xs text-blue-700 dark:text-blue-300">
<strong>Tip:</strong> Consider downloading your current markdown before switching methods to avoid losing your work.
</p>
</div>
</div>
</div>
{/* Footer */}
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700">
<div className="flex justify-end gap-3">
<button
onClick={onCancel}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-md hover:bg-gray-50 dark:hover:bg-gray-500 transition-colors"
>
Cancel
</button>
<button
onClick={onConfirm}
className="px-4 py-2 text-sm font-medium text-white bg-amber-600 hover:bg-amber-700 rounded-md transition-colors flex items-center gap-2"
>
<AlertTriangle className="h-4 w-4" />
Switch & Clear Data
</button>
</div>
</div>
</div>
</div>
);
};
export default MarkdownEditor;