mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-24 15:48:16 +02:00
feat: add markdown export functionality
Implemented comprehensive markdown export for notes and todos with automatic HTML to Markdown conversion. New Features: - Export single note to Markdown file - Export all notes to combined Markdown - Export todos list to Markdown - Smart HTML to Markdown conversion - Preserves formatting, links, lists, and code blocks Files Created: - src/utils/markdownExport.ts - Complete export utilities Desktop (Index.tsx): - Added FileDown icon to imports - New export button in note editor toolbar - Downloads note as .md file with one click Mobile (NoteDetailPage.tsx): - Added "Exporter en Markdown" to dropdown menu - Same functionality as desktop version Markdown Export Features: ✅ Converts HTML formatting to Markdown syntax ✅ Includes note metadata (creation date, tags, priority) ✅ Exports todos as checkboxes [ ] or [x] ✅ Includes image references ✅ Automatic filename generation ✅ One-click download Export includes: - Note title as H1 - Creation date and metadata - Tags with # prefix - Priority indicator ⭐ - Full content with formatting preserved - Todo items with completion status - Image links Perfect for: - Backing up notes - Sharing notes externally - Version control (git) - Using notes in other markdown editors
This commit is contained in:
3 files changed
+268
-2
No files matched your search
+15
-1
@@ -15,7 +15,7 @@ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, Command
|
|||||||
import { RichTextEditor } from "@/components/RichTextEditor";
|
import { RichTextEditor } from "@/components/RichTextEditor";
|
||||||
import {
|
import {
|
||||||
PlusCircle, Search, User, LogOut, Settings, ChevronDown, Plus, Archive, Trash2,
|
PlusCircle, Search, User, LogOut, Settings, ChevronDown, Plus, Archive, Trash2,
|
||||||
Image as ImageIcon, CheckSquare, FileText, Rss, ExternalLink, RefreshCw, Key, Zap, Paperclip, X, Edit, Calendar as CalendarIcon, Tag as TagIcon, MessageSquare, Send, Check, ChevronsUpDown, Star, Activity
|
Image as ImageIcon, CheckSquare, FileText, Rss, ExternalLink, RefreshCw, Key, Zap, Paperclip, X, Edit, Calendar as CalendarIcon, Tag as TagIcon, MessageSquare, Send, Check, ChevronsUpDown, Star, Activity, FileDown
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import AuthService from "@/services/AuthService";
|
import AuthService from "@/services/AuthService";
|
||||||
import AdminService from "@/services/AdminService";
|
import AdminService from "@/services/AdminService";
|
||||||
@@ -33,6 +33,7 @@ import { InputModal } from "@/components/modals/InputModal";
|
|||||||
import { AddUserModal } from "@/components/modals/AddUserModal";
|
import { AddUserModal } from "@/components/modals/AddUserModal";
|
||||||
import { compressImage, formatFileSize } from "@/utils/imageCompression";
|
import { compressImage, formatFileSize } from "@/utils/imageCompression";
|
||||||
import { ModeToggle } from "@/components/mode-toggle";
|
import { ModeToggle } from "@/components/mode-toggle";
|
||||||
|
import { downloadNoteAsMarkdown, downloadAllNotesAsMarkdown, downloadTodosAsMarkdown } from "@/utils/markdownExport";
|
||||||
|
|
||||||
// ===== FONCTIONS UTILITAIRES TIMEZONE EUROPE/PARIS =====
|
// ===== FONCTIONS UTILITAIRES TIMEZONE EUROPE/PARIS =====
|
||||||
|
|
||||||
@@ -1278,6 +1279,19 @@ const Index = () => {
|
|||||||
<div></div>
|
<div></div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => {
|
||||||
|
if (openNote) {
|
||||||
|
downloadNoteAsMarkdown(openNote);
|
||||||
|
showSuccess("Note exportée en Markdown");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title="Exporter en Markdown"
|
||||||
|
>
|
||||||
|
<FileDown className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ import {
|
|||||||
Paperclip,
|
Paperclip,
|
||||||
Tag as TagIcon,
|
Tag as TagIcon,
|
||||||
X,
|
X,
|
||||||
MoreVertical
|
MoreVertical,
|
||||||
|
FileDown
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -28,6 +29,7 @@ import {
|
|||||||
import NotesService, { Note } from "@/services/NotesService";
|
import NotesService, { Note } from "@/services/NotesService";
|
||||||
import { showError, showSuccess } from "@/utils/toast";
|
import { showError, showSuccess } from "@/utils/toast";
|
||||||
import { compressImage, formatFileSize } from "@/utils/imageCompression";
|
import { compressImage, formatFileSize } from "@/utils/imageCompression";
|
||||||
|
import { downloadNoteAsMarkdown } from "@/utils/markdownExport";
|
||||||
|
|
||||||
export default function NoteDetailPage() {
|
export default function NoteDetailPage() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
@@ -199,6 +201,15 @@ export default function NoteDetailPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onClick={() => {
|
||||||
|
if (note) {
|
||||||
|
downloadNoteAsMarkdown(note);
|
||||||
|
showSuccess("Note exportée en Markdown");
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<FileDown className="h-4 w-4 mr-2" />
|
||||||
|
Exporter en Markdown
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={async () => {
|
<DropdownMenuItem onClick={async () => {
|
||||||
if (note?.id) {
|
if (note?.id) {
|
||||||
const newArchived = !note.archived;
|
const newArchived = !note.archived;
|
||||||
|
|||||||
@@ -0,0 +1,241 @@
|
|||||||
|
import { Note } from "@/services/NotesService";
|
||||||
|
import { Todo } from "@/services/TodosService";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convertit le contenu HTML d'une note en Markdown
|
||||||
|
*/
|
||||||
|
function htmlToMarkdown(html: string): string {
|
||||||
|
let markdown = html;
|
||||||
|
|
||||||
|
// Enlever les balises HTML simples et les convertir en Markdown
|
||||||
|
markdown = markdown
|
||||||
|
// Headings
|
||||||
|
.replace(/<h1[^>]*>(.*?)<\/h1>/g, '# $1\n')
|
||||||
|
.replace(/<h2[^>]*>(.*?)<\/h2>/g, '## $1\n')
|
||||||
|
.replace(/<h3[^>]*>(.*?)<\/h3>/g, '### $1\n')
|
||||||
|
// Bold and italic
|
||||||
|
.replace(/<strong[^>]*>(.*?)<\/strong>/g, '**$1**')
|
||||||
|
.replace(/<b[^>]*>(.*?)<\/b>/g, '**$1**')
|
||||||
|
.replace(/<em[^>]*>(.*?)<\/em>/g, '*$1*')
|
||||||
|
.replace(/<i[^>]*>(.*?)<\/i>/g, '*$1*')
|
||||||
|
// Lists
|
||||||
|
.replace(/<ul[^>]*>/g, '\n')
|
||||||
|
.replace(/<\/ul>/g, '\n')
|
||||||
|
.replace(/<ol[^>]*>/g, '\n')
|
||||||
|
.replace(/<\/ol>/g, '\n')
|
||||||
|
.replace(/<li[^>]*>(.*?)<\/li>/g, '- $1\n')
|
||||||
|
// Paragraphs and breaks
|
||||||
|
.replace(/<p[^>]*>/g, '\n')
|
||||||
|
.replace(/<\/p>/g, '\n')
|
||||||
|
.replace(/<br\s*\/?>/g, '\n')
|
||||||
|
// Links
|
||||||
|
.replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/g, '[$2]($1)')
|
||||||
|
// Code
|
||||||
|
.replace(/<code[^>]*>(.*?)<\/code>/g, '`$1`')
|
||||||
|
.replace(/<pre[^>]*>(.*?)<\/pre>/g, '```\n$1\n```')
|
||||||
|
// Remove remaining HTML tags
|
||||||
|
.replace(/<[^>]*>/g, '')
|
||||||
|
// Decode HTML entities
|
||||||
|
.replace(/ /g, ' ')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
// Clean up extra whitespace
|
||||||
|
.replace(/\n{3,}/g, '\n\n')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
return markdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exporte une note au format Markdown
|
||||||
|
*/
|
||||||
|
export function exportNoteAsMarkdown(note: Note): string {
|
||||||
|
let markdown = '';
|
||||||
|
|
||||||
|
// Title
|
||||||
|
markdown += `# ${note.title || 'Sans titre'}\n\n`;
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
if (note.created_at) {
|
||||||
|
const date = new Date(note.created_at);
|
||||||
|
markdown += `*Créée le: ${date.toLocaleDateString('fr-FR')} à ${date.toLocaleTimeString('fr-FR')}*\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tags
|
||||||
|
if (note.tags && note.tags.length > 0) {
|
||||||
|
markdown += `**Tags:** ${note.tags.map(tag => `#${tag.name}`).join(', ')}\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority
|
||||||
|
if (note.priority) {
|
||||||
|
markdown += `⭐ **Prioritaire**\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
markdown += '---\n\n';
|
||||||
|
|
||||||
|
// Content
|
||||||
|
if (note.content) {
|
||||||
|
markdown += htmlToMarkdown(note.content);
|
||||||
|
markdown += '\n\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Todos
|
||||||
|
if (note.todos && note.todos.length > 0) {
|
||||||
|
markdown += '## Tâches\n\n';
|
||||||
|
note.todos.forEach(todo => {
|
||||||
|
const checkbox = todo.completed ? '[x]' : '[ ]';
|
||||||
|
markdown += `- ${checkbox} ${todo.text}\n`;
|
||||||
|
});
|
||||||
|
markdown += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Images
|
||||||
|
if (note.images && note.images.length > 0) {
|
||||||
|
markdown += '## Images\n\n';
|
||||||
|
note.images.forEach(image => {
|
||||||
|
markdown += `\n`;
|
||||||
|
});
|
||||||
|
markdown += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
return markdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Télécharge une note au format Markdown
|
||||||
|
*/
|
||||||
|
export function downloadNoteAsMarkdown(note: Note): void {
|
||||||
|
const markdown = exportNoteAsMarkdown(note);
|
||||||
|
const blob = new Blob([markdown], { type: 'text/markdown;charset=utf-8' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = `${note.title || 'note'}.md`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exporte toutes les notes au format Markdown (un fichier par note dans un zip)
|
||||||
|
*/
|
||||||
|
export function exportAllNotesAsMarkdown(notes: Note[]): string {
|
||||||
|
let combined = `# Mes Notes\n\n`;
|
||||||
|
combined += `*Exporté le ${new Date().toLocaleDateString('fr-FR')} à ${new Date().toLocaleTimeString('fr-FR')}*\n\n`;
|
||||||
|
combined += `---\n\n`;
|
||||||
|
|
||||||
|
notes.forEach((note, index) => {
|
||||||
|
if (index > 0) {
|
||||||
|
combined += `\n\n---\n\n`;
|
||||||
|
}
|
||||||
|
combined += exportNoteAsMarkdown(note);
|
||||||
|
});
|
||||||
|
|
||||||
|
return combined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Télécharge toutes les notes dans un fichier Markdown
|
||||||
|
*/
|
||||||
|
export function downloadAllNotesAsMarkdown(notes: Note[]): void {
|
||||||
|
const markdown = exportAllNotesAsMarkdown(notes);
|
||||||
|
const blob = new Blob([markdown], { type: 'text/markdown;charset=utf-8' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
const filename = `noteflow-export-${new Date().toISOString().split('T')[0]}.md`;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exporte un todo au format Markdown
|
||||||
|
*/
|
||||||
|
export function exportTodoAsMarkdown(todo: Todo): string {
|
||||||
|
const checkbox = todo.completed ? '[x]' : '[ ]';
|
||||||
|
let markdown = `- ${checkbox} ${todo.text}`;
|
||||||
|
|
||||||
|
if (todo.priority) {
|
||||||
|
markdown += ' ⭐';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (todo.in_progress) {
|
||||||
|
markdown += ' 🔄';
|
||||||
|
}
|
||||||
|
|
||||||
|
return markdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exporte tous les todos au format Markdown
|
||||||
|
*/
|
||||||
|
export function exportTodosAsMarkdown(todos: Todo[]): string {
|
||||||
|
let markdown = '# Mes Tâches\n\n';
|
||||||
|
markdown += `*Exporté le ${new Date().toLocaleDateString('fr-FR')} à ${new Date().toLocaleTimeString('fr-FR')}*\n\n`;
|
||||||
|
|
||||||
|
// Tâches prioritaires
|
||||||
|
const priorityTodos = todos.filter(t => t.priority && !t.completed);
|
||||||
|
if (priorityTodos.length > 0) {
|
||||||
|
markdown += '## ⭐ Prioritaires\n\n';
|
||||||
|
priorityTodos.forEach(todo => {
|
||||||
|
markdown += exportTodoAsMarkdown(todo) + '\n';
|
||||||
|
});
|
||||||
|
markdown += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tâches en cours
|
||||||
|
const inProgressTodos = todos.filter(t => t.in_progress && !t.completed);
|
||||||
|
if (inProgressTodos.length > 0) {
|
||||||
|
markdown += '## 🔄 En cours\n\n';
|
||||||
|
inProgressTodos.forEach(todo => {
|
||||||
|
markdown += exportTodoAsMarkdown(todo) + '\n';
|
||||||
|
});
|
||||||
|
markdown += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tâches en attente
|
||||||
|
const pendingTodos = todos.filter(t => !t.completed && !t.priority && !t.in_progress);
|
||||||
|
if (pendingTodos.length > 0) {
|
||||||
|
markdown += '## 📋 En attente\n\n';
|
||||||
|
pendingTodos.forEach(todo => {
|
||||||
|
markdown += exportTodoAsMarkdown(todo) + '\n';
|
||||||
|
});
|
||||||
|
markdown += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tâches terminées
|
||||||
|
const completedTodos = todos.filter(t => t.completed);
|
||||||
|
if (completedTodos.length > 0) {
|
||||||
|
markdown += '## ✅ Terminées\n\n';
|
||||||
|
completedTodos.forEach(todo => {
|
||||||
|
markdown += exportTodoAsMarkdown(todo) + '\n';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return markdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Télécharge tous les todos au format Markdown
|
||||||
|
*/
|
||||||
|
export function downloadTodosAsMarkdown(todos: Todo[]): void {
|
||||||
|
const markdown = exportTodosAsMarkdown(todos);
|
||||||
|
const blob = new Blob([markdown], { type: 'text/markdown;charset=utf-8' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
const filename = `noteflow-todos-${new Date().toISOString().split('T')[0]}.md`;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user