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

7.9 KiB
Raw Permalink Blame History

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 :

// 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 :

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