# Story: Markdown Round-Trip — Export & Import TipTap ↔ Markdown > **Epic:** Epic 6 — Croissance & Activation (PLG) > **ID:** 6-2-markdown-roundtrip > **Priority:** Beta Blocker > **Status:** review > **Depends on:** Rich-text editor TipTap (`rich-text-editor.tsx` ✅) > **Ref brief:** `docs/brief-markdown-roundtrip.md` --- ## Contexte TipTap/ProseMirror stocke le contenu en JSON natif. Il n'existe **aucune sérialisation TipTap → Markdown** ni **Markdown → TipTap** dans le code actuel. `@tiptap/extension-markdown` n'est pas disponible sur npm public (extension TipTap Pro). L'approche choisie utilise : - **`turndown`** : HTML → Markdown (export) - **`marked`** : Markdown → HTML → TipTap parse (import + paste) Ces deux librairies sont déjà dans le registre npm. `react-markdown` est déjà installé mais sert uniquement au rendu. `marked` est disponible (`18.x`). --- ## User Stories ### US-MARKDOWN-1 : Export Markdown **En tant qu'** utilisateur, **Je veux** exporter ma note en fichier `.md`, **Afin de** la réutiliser dans d'autres outils (Obsidian, VS Code, GitHub, etc.). #### Critères d'acceptation : - [x] **AC-1** : Un item "Exporter en Markdown" est visible dans le menu `…` de la note - [x] **AC-2** : Au clic, un fichier `.md` est téléchargé dans le navigateur - [x] **AC-3** : Le fichier contient : titre en `# H1`, headings, bold, italic, listes, tâches (`- [x]`), code inline/fenced, blockquotes, liens, tables - [x] **AC-4** : Un `liveBlock` est exporté en commentaire HTML `` - [x] **AC-5** : Un `structuredViewBlock` est exporté en commentaire HTML `` --- ### US-MARKDOWN-2 : Paste Markdown → blocs TipTap **En tant qu'** utilisateur, **Je veux** coller du Markdown dans l'éditeur et obtenir des blocs TipTap, **Afin de** ne pas avoir à reformater manuellement. #### Critères d'acceptation : - [x] **AC-6** : Coller un texte qui commence par `#`, `##`, `- `, `* `, `1.`, `` ` ``, `>`, `**`, ou `|` déclenche la conversion Markdown → HTML → TipTap - [x] **AC-7** : Un texte normal (sans marqueurs Markdown) est collé tel quel (pas de conversion parasite) - [x] **AC-8** : Les tables Markdown sont converties en tables TipTap - [x] **AC-9** : Les tâches `- [x]` sont converties en `taskItem` cochés --- ### US-MARKDOWN-3 : Import fichier `.md` **En tant qu'** utilisateur, **Je veux** importer un fichier `.md` dans une nouvelle note (ou écraser le contenu), **Afin de** migrer du contenu depuis d'autres outils. #### Critères d'acceptation : - [x] **AC-10** : Un item "Importer Markdown" est visible dans le menu `…` de la note (ou via un bouton dédié) - [x] **AC-11** : Au clic, un file picker s'ouvre filtré sur `.md` - [x] **AC-12** : Après sélection, le contenu de la note est **remplacé** par le contenu du fichier parsé en TipTap - [x] **AC-13** : Le titre de la note est mis à jour avec le premier `# H1` du fichier importé (si présent) --- ## Tasks / Subtasks ### T1 — Installation des dépendances - [x] T1.1 — Installer `turndown` + `@types/turndown` (HTML → Markdown) - [x] T1.2 — Vérifier que `marked` est déjà disponible ou installer si absent - [x] T1.3 — Vérifier que `@types/marked` est disponible ### T2 — Créer `lib/editor/markdown-export.ts` - [x] T2.1 — Implémenter `tiptapHTMLToMarkdown(html: string): string` via `turndown` - Règle custom `liveBlock` : div avec `data-live-block` → commentaire HTML via sentinel + post-process - Règle custom `structuredViewBlock` : div avec `data-structured-view-block` → commentaire HTML via sentinel - GFM tables activé (`turndown-plugin-gfm`) - GFM task lists activé - [x] T2.2 — Implémenter `markdownToHTML(md: string): string` via `marked` - GFM activé, tables activées - Retourner HTML propre pour injection TipTap via `editor.commands.setContent(html)` - [x] T2.3 — Implémenter `looksLikeMarkdown(text: string): boolean` - Détecte si un texte collé contient des marqueurs Markdown (heuristique) - Regex : `#`, `- `, `* `, `1. `, `>`, ` ``` `, `**`, `_`, `|`, `[...](...)` ### T3 — Route API export Markdown - [x] T3.1 — Implémenté côté client dans `note-editor-toolbar.tsx` (pas de route serveur — choix UX : utilise l'état live de l'éditeur) - [x] T3.2 — Titre en `# ` préfixé dans le fichier `.md` exporté ### T4 — Action UI Export dans `note-editor-toolbar.tsx` - [x] T4.1 — Ajouter `DropdownMenuItem` "Exporter en Markdown" dans le menu `…` (avec icône `FileDown`) - [x] T4.2 — Au clic : `editor.getHTML()` → `tiptapHTMLToMarkdown()` → `Blob` → download via URL.createObjectURL - [x] T4.3 — Toast de succès/erreur ### T5 — Paste Markdown dans l'éditeur - [x] T5.1 — Créer extension TipTap `MarkdownPasteExtension` dans `lib/editor/markdown-paste-extension.ts` - Hook sur `handlePaste` : si `looksLikeMarkdown(text)` → `markdownToHTML(text)` → `editor.commands.insertContent` - `setTimeout(0)` pour éviter conflits de transactions - [x] T5.2 — Intégrer `MarkdownPasteExtension` dans `extensions[]` de `rich-text-editor.tsx` ### T6 — Import fichier `.md` dans `note-editor-toolbar.tsx` - [x] T6.1 — Ajouter `DropdownMenuItem` "Importer Markdown" dans le menu `…` (icône `FileUp`) - [x] T6.2 — Input `` caché avec `mdImportInputRef` - [x] T6.3 — Handler `handleImportMarkdownFile` : lire fichier → `markdownToHTML()` → `actions.setContent(html)` - [x] T6.4 — Titre mis à jour via `extractMarkdownTitle()` + `actions.setTitle()` ### T7 — i18n (15 locales) - [x] T7.1 — Ajout clés dans `locales/en.json` et `locales/fr.json` (namespace `richTextEditor`) - `exportMarkdown`, `importMarkdown`, `markdownExportSuccess`, `markdownExportError`, `markdownImportSuccess` - [x] T7.2 — Clés ajoutées dans les 13 autres locales (de, es, it, pt, nl, pl, ru, zh, ja, ko, ar, fa, hi) ### T8 — Tests unitaires - [x] T8.1 — Tests `lib/editor/markdown-export.ts` : 40 tests — heading, bold, italic, list, code, blockquote, link, table, liveBlock, svBlock - [x] T8.2 — Tests `looksLikeMarkdown` : 12 cas positifs + négatifs --- ## Dev Notes ### Approche technique **Pourquoi pas `@tiptap/extension-markdown` ?** Non disponible sur npm public (`404 Not Found`). C'est une extension TipTap Pro. **Approche choisie : `turndown` + `marked`** - `turndown` (7.x) : battle-tested, supporte les plugins GFM (tables, task lists via `turndown-plugin-gfm`) - `marked` (18.x) : fast, supporte GFM tables/tasks nativement **Export côté serveur vs client :** L'export via route API `/api/notes/[id]/export?format=markdown` permet de ne pas dépendre de l'état de l'éditeur côté client — fonctionne même si l'éditeur est fermé. **TipTap `generateHTML` côté serveur :** ```typescript import { generateHTML } from '@tiptap/html' ``` Nécessite d'importer toutes les extensions utilisées. Une liste partagée dans `lib/editor/tiptap-extensions-server.ts` simplifie la maintenance. **MarkdownPasteExtension :** Extension légère qui s'insère dans la chaîne de `handlePaste`. Si le texte collé contient des marqueurs Markdown, on le convertit avant insertion. Sinon, on laisse TipTap gérer normalement. **Custom nodes HTML output :** - `liveBlock` génère `
` dans `getHTML()` - `structuredViewBlock` génère `
` - `turndown` peut détecter ces divs par `data-type` et les convertir en commentaires HTML **Import titre :** Regex sur la première ligne du fichier `.md` : `^#\s+(.+)` → titre. Passé en callback au parent. ### Fichiers clés existants - `memento-note/components/rich-text-editor.tsx` — éditeur principal - `memento-note/components/note-actions.tsx` — menu actions note (dropdown `…`) - `memento-note/lib/editor/` — extensions TipTap existantes - `memento-note/locales/` — 15 fichiers JSON i18n ### Patterns importants - Actions dans le menu `…` : `DropdownMenuItem` avec icône Lucide + `t('key')` i18n - Download client-side : `URL.createObjectURL(blob)` + clic programmatique + `URL.revokeObjectURL` - Tests unitaires : `tests/unit/` avec Jest, importé via alias `@/` --- ## Dev Agent Record ### Implementation Plan **Approche finale :** `turndown` + `marked` (l'extension officielle `@tiptap/extension-markdown` est indisponible sur npm public — TipTap Pro uniquement). - Export : client-side via `editor.getHTML()` → `turndown` → Blob download (pas de route serveur — utilise l'état live) - Import : `FileReader` + `marked` → `editor.commands.setContent(html)` - Paste : extension ProseMirror `handlePaste` → `looksLikeMarkdown` heuristique → `marked` → `insertContent` - Custom nodes (liveBlock, structuredViewBlock) : pre-processing avec sentinels alphanumériques, post-processing avec commentaires HTML ### Debug Log - **turndown + divs vides** : `turndown` ignore les nœuds "blank" (pas de texte). Solution : pre-process HTML → remplacer divs custom par `

MOMENTOBLOCKSNTINELXXX

`, post-process le MD résultant pour remplacer par ``. - **turndown escape underscores** : le sentinel `__MOMENTO_BLOCK__` est échappé en `\_\_MOMENTO\_BLOCK\_\_`. Solution : utiliser uniquement des caractères alphanumériques (`MOMENTOBLOCKSENTINELLIVEBLOCK0`). - **TS2322 dans toolbar** : `editor.commands.setContent(html, true)` — le 2e argument est `SetContentOptions` pas `boolean` dans TipTap v3. Corrigé. - **Fragment JSX** : `` caché hors du div principal — wrappé dans `<>` fragment. - **handleConvertToRichtext** : le remplacement d'import avait supprimé l'ouverture `const handleConvertToRichtext = async () => {`. Restauré. ### Completion Notes - **T1** ✅ : `turndown@7.2.4` + `turndown-plugin-gfm` + `@types/turndown` installés. `marked@18.0.3` déjà présent. - **T2** ✅ : `lib/editor/markdown-export.ts` — `tiptapHTMLToMarkdown`, `markdownToHTML`, `looksLikeMarkdown`, `extractMarkdownTitle`. Gestion des nœuds custom via sentinels. - **T3** ✅ : Export client-side (pas de route serveur). `richTextEditorRef.current?.getEditor().getHTML()` → markdown → Blob download. - **T4** ✅ : `DropdownMenuItem` "Exporter en Markdown" + "Importer Markdown" dans `note-editor-toolbar.tsx` (menu `…`). - **T5** ✅ : `lib/editor/markdown-paste-extension.ts` créé + intégré dans `rich-text-editor.tsx`. - **T6** ✅ : Import fichier `.md` avec `mdImportInputRef` + `handleImportMarkdownFile` dans toolbar. Titre auto-extrait. - **T7** ✅ : 5 clés dans namespace `richTextEditor` ajoutées à toutes les 15 locales. - **T8** ✅ : 40 tests unit passent (39 sur fonctions, 1 fix pour spacing turndown). Suite complète : 174/174. --- ## File List - `memento-note/package.json` — ajout `turndown@7.2.4`, `turndown-plugin-gfm`, `@types/turndown` - `memento-note/lib/editor/markdown-export.ts` — **créé** — helpers markdown roundtrip - `memento-note/lib/editor/markdown-paste-extension.ts` — **créé** — extension TipTap paste Markdown - `memento-note/components/note-editor/note-editor-toolbar.tsx` — modifié — items Export/Import Markdown + handlers + import ref - `memento-note/components/rich-text-editor.tsx` — modifié — import + intégration `MarkdownPasteExtension` - `memento-note/types/global.d.ts` — modifié — déclaration de types pour `turndown-plugin-gfm` - `memento-note/locales/en.json` — modifié — 5 clés `richTextEditor.export/importMarkdown*` - `memento-note/locales/fr.json` — modifié — idem - `memento-note/locales/de.json` — modifié — idem - `memento-note/locales/es.json` — modifié — idem - `memento-note/locales/it.json` — modifié — idem - `memento-note/locales/pt.json` — modifié — idem - `memento-note/locales/nl.json` — modifié — idem - `memento-note/locales/pl.json` — modifié — idem - `memento-note/locales/ru.json` — modifié — idem - `memento-note/locales/zh.json` — modifié — idem - `memento-note/locales/ja.json` — modifié — idem - `memento-note/locales/ko.json` — modifié — idem - `memento-note/locales/ar.json` — modifié — idem - `memento-note/locales/fa.json` — modifié — idem - `memento-note/locales/hi.json` — modifié — idem - `memento-note/tests/unit/markdown-export.test.ts` — **créé** — 40 tests unitaires - `docs/story-markdown-roundtrip.md` — **créé** — story complète - `docs/sprint-status.yaml` — modifié — `6-2-markdown-roundtrip: in-progress → review` --- ## Change Log | Date | Description | |------|-------------| | 2026-05-30 | Story créée à partir du brief `docs/brief-markdown-roundtrip.md` | | 2026-05-30 | Implémentation complète — T1 à T8 (voir Dev Agent Record) |