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>
224 lines
7.9 KiB
Markdown
224 lines
7.9 KiB
Markdown
# Brief Technique : US-EDITOR-MARKDOWN — Round-Trip TipTap ↔ Markdown
|
||
|
||
> **ID:** US-EDITOR-MARKDOWN
|
||
> **Priorité:** Beta blocker (confirmé)
|
||
> **Dépendances:** Rich-text editor TipTap existant (`rich-text-editor.tsx`)
|
||
> **Complexité estimée:** Moyenne (2–5 jours selon approche choisie)
|
||
|
||
---
|
||
|
||
## Contexte
|
||
|
||
TipTap/ProseMirror stocke le contenu en **JSON natif** (format ProseMirror Doc). Actuellement :
|
||
- L'éditeur **lit et écrit du JSON** en base de données
|
||
- `marked`, `react-markdown`, `remark-gfm` sont installés mais utilisés uniquement pour **l'affichage** de contenu Markdown externe (Web Clipper, import)
|
||
- Il **n'existe aucune sérialisation TipTap → Markdown** ni **Markdown → TipTap** dans le code actuel
|
||
|
||
L'objectif est un **round-trip fidèle** :
|
||
```
|
||
Note TipTap (JSON) → Export Markdown → Re-import → JSON identique (byte-for-byte sur les éléments supportés)
|
||
```
|
||
|
||
---
|
||
|
||
## Analyse des Options
|
||
|
||
### Option A — `@tiptap/extension-markdown` (recommandé ✅)
|
||
|
||
**Package :** `@tiptap/extension-markdown` (officiel TipTap, payant pour certaines extensions avancées — vérifier licence)
|
||
|
||
**Principe :**
|
||
- Extension TipTap officielle qui ajoute une méthode `.storage.markdown.getMarkdown()` sur l'éditeur
|
||
- Import Markdown via `editor.commands.setContent(markdownString, { parseOptions: { markdown: true } })`
|
||
- Supporte GFM (GitHub Flavored Markdown) : tableaux, listes de tâches, code fences
|
||
|
||
**Avantages :**
|
||
- Intégration native TipTap — zéro friction
|
||
- Maintenu par l'équipe TipTap
|
||
- Support des nœuds custom via `markdownSerializer` sur chaque extension
|
||
|
||
**Inconvénients :**
|
||
- Certaines extensions sont sous licence TipTap Pro ($149/mois)
|
||
- Les nœuds custom de Momento (`liveBlock`, `structuredViewBlock`) nécessitent des serializers manuels
|
||
- Round-trip parfait impossible pour ces nœuds (dégradation gracieuse : placeholder HTML comment)
|
||
|
||
**Implémentation :**
|
||
```typescript
|
||
// Installation
|
||
npm install @tiptap/extension-markdown
|
||
|
||
// Dans rich-text-editor.tsx
|
||
import { Markdown } from '@tiptap/extension-markdown'
|
||
|
||
const editor = useEditor({
|
||
extensions: [
|
||
// ... extensions existantes ...
|
||
Markdown.configure({
|
||
html: false, // Désactiver HTML brut dans le MD
|
||
tightLists: true, // Listes compactes
|
||
tightListClass: 'tight',
|
||
bulletListMarker: '-',
|
||
linkify: false,
|
||
breaks: false,
|
||
transformPastedText: true, // Coller du Markdown → conversion auto
|
||
transformCopiedText: false,
|
||
}),
|
||
],
|
||
})
|
||
|
||
// Export
|
||
const markdown = editor.storage.markdown.getMarkdown()
|
||
|
||
// Import
|
||
editor.commands.setContent(markdownString)
|
||
```
|
||
|
||
---
|
||
|
||
### Option B — `prosemirror-markdown` (alternative robuste ✅)
|
||
|
||
**Package :** `prosemirror-markdown` (officiel ProseMirror)
|
||
|
||
**Principe :**
|
||
- Bibliothèque bas niveau qui fournit un `MarkdownSerializer` et un `MarkdownParser`
|
||
- S'intègre à TipTap via le schéma ProseMirror sous-jacent
|
||
- Utilise `remark` (déjà installé) pour le parsing
|
||
|
||
**Avantages :**
|
||
- Open source, pas de licence Pro
|
||
- Contrôle total du serializer (chaque nœud est défini explicitement)
|
||
- Utilisé en production par de nombreux éditeurs (GitLab, Linear)
|
||
|
||
**Inconvénients :**
|
||
- Plus verbeux — chaque extension TipTap nécessite son `toMarkdown` et `fromMarkdown`
|
||
- Effort initial plus élevé (~2 jours de mapping)
|
||
- À maintenir à chaque nouvelle extension ajoutée
|
||
|
||
**Implémentation :**
|
||
```typescript
|
||
import { MarkdownSerializer, defaultMarkdownSerializer } from 'prosemirror-markdown'
|
||
import { MarkdownParser } from 'prosemirror-markdown'
|
||
import markdownit from 'markdown-it'
|
||
|
||
// Serializer — mapper chaque nœud TipTap
|
||
const momentoSerializer = new MarkdownSerializer(
|
||
{
|
||
...defaultMarkdownSerializer.nodes,
|
||
// Nœuds Momento custom
|
||
liveBlock: (state, node) => {
|
||
state.write(`<!-- live-block: ${node.attrs.sourceNoteId}#${node.attrs.blockId} -->`)
|
||
state.closeBlock(node)
|
||
},
|
||
structuredViewBlock: (state, node) => {
|
||
state.write(`<!-- structured-view: ${JSON.stringify(node.attrs)} -->`)
|
||
state.closeBlock(node)
|
||
},
|
||
},
|
||
defaultMarkdownSerializer.marks
|
||
)
|
||
|
||
// Export
|
||
const markdown = momentoSerializer.serialize(editor.state.doc)
|
||
|
||
// Parser
|
||
const md = markdownit('commonmark', { html: true })
|
||
const parser = new MarkdownParser(editor.schema, md, { /* token map */ })
|
||
const doc = parser.parse(markdownString)
|
||
editor.commands.setContent(doc.toJSON())
|
||
```
|
||
|
||
---
|
||
|
||
### Option C — `Milkdown` (remplacement complet ❌ déconseillé)
|
||
|
||
**Milkdown** est un éditeur Markdown-first qui remplace TipTap/ProseMirror. Le migrer vers Milkdown signifie :
|
||
- Réécriture complète de `rich-text-editor.tsx` (~800 lignes)
|
||
- Perte de toutes les extensions custom (Living Blocks, Structured Views, Smart Paste, etc.)
|
||
- Délai estimé : 2-4 semaines
|
||
|
||
**Verdict : ❌ Trop risqué, trop long pour la beta.**
|
||
|
||
---
|
||
|
||
## Recommandation
|
||
|
||
### Court terme (beta) : **Option A** (`@tiptap/extension-markdown`)
|
||
|
||
Raisons :
|
||
1. Intégration en **1 journée** dans l'éditeur existant
|
||
2. Couvre 95% des cas d'usage (texte, listes, headings, code, tables, tâches)
|
||
3. Le `transformPastedText: true` résout aussi un bug UX courant (coller du Markdown brut)
|
||
4. Les nœuds Momento non-supportés sont préservés via commentaires HTML (dégradation gracieuse)
|
||
|
||
### Long terme : **Option B** en complément
|
||
|
||
Pour les cas avancés (export propre des nœuds custom, CI de round-trip byte-for-byte), implémenter Option B en remplacement d'Option A une fois la beta stabilisée.
|
||
|
||
---
|
||
|
||
## Scope de l'US-EDITOR-MARKDOWN (Beta)
|
||
|
||
### Ce qui est inclus
|
||
|
||
| Feature | Priorité |
|
||
|---------|----------|
|
||
| Export note → fichier `.md` téléchargeable | 🔴 P0 |
|
||
| Coller du Markdown → conversion auto en blocs TipTap | 🔴 P0 |
|
||
| Import note depuis fichier `.md` | 🟡 P1 |
|
||
| Copy note as Markdown (dans le presse-papier) | 🟡 P1 |
|
||
| Round-trip fidèle pour : headings, bold, italic, lists, tasks, code, blockquote, links, tables | 🔴 P0 |
|
||
| Dégradation gracieuse pour `liveBlock` et `structuredViewBlock` (commentaire HTML) | 🟡 P1 |
|
||
|
||
### Ce qui est exclu (post-beta)
|
||
|
||
- Round-trip byte-for-byte des nœuds Momento custom
|
||
- Édition native en mode source Markdown (raw text editor)
|
||
- Sync bidirectionnelle temps réel Markdown ↔ TipTap
|
||
|
||
---
|
||
|
||
## Fichiers à créer / modifier
|
||
|
||
| Fichier | Action |
|
||
|---------|--------|
|
||
| `memento-note/package.json` | Ajouter `@tiptap/extension-markdown` |
|
||
| `components/rich-text-editor.tsx` | Ajouter extension `Markdown` à la config TipTap |
|
||
| `lib/editor/markdown-export.ts` | Créer — helper `tiptapDocToMarkdown(doc)` et `markdownToTiptapDoc(md)` |
|
||
| `components/note-actions.tsx` | Ajouter action "Exporter en Markdown" |
|
||
| `app/api/notes/[id]/export/route.ts` | Créer — `GET /api/notes/:id/export?format=markdown` |
|
||
| `locales/en.json` + `fr.json` | Ajouter clés `editor.exportMarkdown`, `editor.importMarkdown`, `editor.pasteMarkdown` |
|
||
|
||
---
|
||
|
||
## Critères d'acceptation
|
||
|
||
**Given** j'ai une note avec du contenu riche (headings, listes, code, tasks)
|
||
**When** je clique "Exporter en Markdown"
|
||
**Then** je télécharge un fichier `.md` avec le contenu fidèlement sérialisé
|
||
|
||
**Given** je copie du Markdown depuis un éditeur externe
|
||
**When** je colle dans l'éditeur Momento
|
||
**Then** le Markdown est automatiquement converti en blocs TipTap correspondants (pas du texte brut)
|
||
|
||
**Given** j'importe un fichier `.md`
|
||
**When** le parsing est terminé
|
||
**Then** les headings/listes/code/tables sont correctement représentés dans l'éditeur
|
||
|
||
**Given** ma note contient un `liveBlock` (bloc vivant)
|
||
**When** j'exporte en Markdown
|
||
**Then** le bloc vivant est exporté en commentaire HTML `<!-- live-block: ... -->` sans erreur
|
||
|
||
---
|
||
|
||
## Estimation
|
||
|
||
| Tâche | Durée estimée |
|
||
|-------|--------------|
|
||
| Installation + config `@tiptap/extension-markdown` | 2h |
|
||
| Helper `lib/editor/markdown-export.ts` | 2h |
|
||
| Route API export + action UI | 2h |
|
||
| Import fichier `.md` via modal | 3h |
|
||
| Tests manuels round-trip + fix edge cases | 4h |
|
||
| i18n (EN + FR) | 1h |
|
||
| **Total** | **~2 jours** |
|