Files
Momento/docs/story-markdown-roundtrip.md
Antigravity 6b4ed8514f
Some checks failed
CI / Lint, Unit Tests & Build (push) Successful in 5m37s
CI / Deploy production (on server) (push) Has been cancelled
Epic 6: Stories 6-2 (Markdown roundtrip) + 6-3 (Brainstorm PPTX + Canvas)
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>
2026-05-29 11:24:56 +00:00

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) |