Files
Momento/docs/brief-markdown-roundtrip.md
Antigravity 96e7902f01
Some checks failed
CI / Lint, Unit Tests & Build (push) Failing after 1m22s
CI / Deploy production (on server) (push) Has been skipped
feat: publication IA (magazine/brief/essay) + fixes critique
Publication IA:
- 4 templates (magazine, brief, essay, simple) avec CSS riche
- Rewrite IA (article/exercises/tutorial/reference/mixed)
- Modération avec timeout 12s + fallback safe
- Quotas publish_enhance par tier (basic=2, pro=15, business=100)
- Détection contenu stale (hash)
- Migration DB publishedContent/publishedTemplate/publishedSourceHash

Fixes:
- cheerio v1.2: Element -> AnyNode (domhandler), decodeEntities cast
- _isShared ajouté au type Note (champ virtuel serveur)
- callout colors PDF export: extraction fonction pure testable
- admin/published: guard note.userId null
- Cmd+S fonctionne en mode dialog (pas seulement fullPage)

i18n:
- 23 clés publish* traduites dans les 15 locales
- Extension Web Clipper: 13 locales mise à jour

Tests:
- callout-colors.test.ts (6 tests)
- note-visible-in-view.test.ts (5 tests)
- entitlements.test.ts + byok-entitlements.test.ts: mock usageLog + unstubAllEnvs
- 199/199 tests passent

Tracker: user-stories.md sync avec sprint-status.yaml
2026-06-28 07:32:57 +00:00

224 lines
7.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (25 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 Memento (`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 Memento 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 Memento 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 Memento 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 Memento
**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** |