Story 6-2 — Markdown roundtrip export/import: - lib/editor/markdown-export.ts: tiptapHTMLToMarkdown, markdownToHTML, looksLikeMarkdown - lib/editor/markdown-paste-extension.ts: TipTap extension paste Markdown → blocs - note-editor-toolbar.tsx: export .md + import .md (file picker) - rich-text-editor.tsx: intégration MarkdownPasteExtension - 40 tests unitaires markdown-export.test.ts Story 6-3 — Brainstorm PPTX + Canvas: - lib/brainstorm/export-pptx.ts: génération PPTX 5 slides (pptxgenjs) - app/api/brainstorm/[sessionId]/export-pptx/route.ts: route POST protégée - brainstorm-page.tsx: bouton PPTX, auto-select session, fix emoji, fix router.replace - wave-canvas.tsx: fitTrigger recentrage, légende bas-droite Onboarding activation wizard (Story 6-1): - components/onboarding/: wizard multi-étapes, hints éditeur - app/api/onboarding/: route PATCH onboarding - prisma/migrations: champs onboarding user Locales: 15 langues mises à jour (brainstorm, markdown, onboarding keys) Sprint: 6-1 done, 6-2 review, 6-3 review Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
240 lines
13 KiB
Markdown
240 lines
13 KiB
Markdown
# 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 `<titre-note>.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 `<!-- live-block: sourceNoteId#blockId -->`
|
|
- [x] **AC-5** : Un `structuredViewBlock` est exporté en commentaire HTML `<!-- structured-view: {...attrs} -->`
|
|
|
|
---
|
|
|
|
### 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 `# <note.title>` 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 `<input type="file" accept=".md">` 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 `<div data-type="live-block" data-source-note-id="..." data-block-id="..."></div>` dans `getHTML()`
|
|
- `structuredViewBlock` génère `<div data-type="structured-view-block" ...></div>`
|
|
- `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 `<p>MOMENTOBLOCKSNTINELXXX</p>`, post-process le MD résultant pour remplacer par `<!-- comment -->`.
|
|
- **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** : `<input type="file">` 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) |
|