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

13 KiB

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 :

  • AC-1 : Un item "Exporter en Markdown" est visible dans le menu de la note
  • AC-2 : Au clic, un fichier <titre-note>.md est téléchargé dans le navigateur
  • AC-3 : Le fichier contient : titre en # H1, headings, bold, italic, listes, tâches (- [x]), code inline/fenced, blockquotes, liens, tables
  • AC-4 : Un liveBlock est exporté en commentaire HTML <!-- live-block: sourceNoteId#blockId -->
  • 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 :

  • AC-6 : Coller un texte qui commence par #, ##, - , * , 1., `, >, **, ou | déclenche la conversion Markdown → HTML → TipTap
  • AC-7 : Un texte normal (sans marqueurs Markdown) est collé tel quel (pas de conversion parasite)
  • AC-8 : Les tables Markdown sont converties en tables TipTap
  • 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 :

  • AC-10 : Un item "Importer Markdown" est visible dans le menu de la note (ou via un bouton dédié)
  • AC-11 : Au clic, un file picker s'ouvre filtré sur .md
  • AC-12 : Après sélection, le contenu de la note est remplacé par le contenu du fichier parsé en TipTap
  • 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

  • T1.1 — Installer turndown + @types/turndown (HTML → Markdown)
  • T1.2 — Vérifier que marked est déjà disponible ou installer si absent
  • T1.3 — Vérifier que @types/marked est disponible

T2 — Créer lib/editor/markdown-export.ts

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

  • 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)
  • T3.2 — Titre en # <note.title> préfixé dans le fichier .md exporté

T4 — Action UI Export dans note-editor-toolbar.tsx

  • T4.1 — Ajouter DropdownMenuItem "Exporter en Markdown" dans le menu (avec icône FileDown)
  • T4.2 — Au clic : editor.getHTML()tiptapHTMLToMarkdown()Blob → download via URL.createObjectURL
  • T4.3 — Toast de succès/erreur

T5 — Paste Markdown dans l'éditeur

  • 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
  • T5.2 — Intégrer MarkdownPasteExtension dans extensions[] de rich-text-editor.tsx

T6 — Import fichier .md dans note-editor-toolbar.tsx

  • T6.1 — Ajouter DropdownMenuItem "Importer Markdown" dans le menu (icône FileUp)
  • T6.2 — Input <input type="file" accept=".md"> caché avec mdImportInputRef
  • T6.3 — Handler handleImportMarkdownFile : lire fichier → markdownToHTML()actions.setContent(html)
  • T6.4 — Titre mis à jour via extractMarkdownTitle() + actions.setTitle()

T7 — i18n (15 locales)

  • T7.1 — Ajout clés dans locales/en.json et locales/fr.json (namespace richTextEditor)
    • exportMarkdown, importMarkdown, markdownExportSuccess, markdownExportError, markdownImportSuccess
  • 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

  • T8.1 — Tests lib/editor/markdown-export.ts : 40 tests — heading, bold, italic, list, code, blockquote, link, table, liveBlock, svBlock
  • 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 :

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 + markededitor.commands.setContent(html)
  • Paste : extension ProseMirror handlePastelooksLikeMarkdown heuristique → markedinsertContent
  • 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.tstiptapHTMLToMarkdown, 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.tscréé — helpers markdown roundtrip
  • memento-note/lib/editor/markdown-paste-extension.tscréé — 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.tscréé — 40 tests unitaires
  • docs/story-markdown-roundtrip.mdcréé — 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)