Resumen
A partir del commit 6a3b279 (2026-05-08), el módulo Cuaderno (sección Tareas y Notas del Workspace) cambia su formato de persistencia interno de JSON TipTap a Markdown como formato canónico, y añade exportación en tres formatos: texto plano, Markdown y HTML autónomo.
Storage canónico: de JSON a Markdown
Motivación
El formato anterior serializaba el contenido de cada nota como JSON.stringify(editor.getJSON()), el objeto interno de TipTap. Esto creaba acoplamiento fuerte con la versión del editor y hacía el contenido opaco en la base de datos D1.
El nuevo formato serializa con tiptap-markdown.getMarkdown(), obteniendo Markdown estándar legible directamente en D1.
Limitación conocida y aceptada: el color de texto no tiene representación en Markdown estándar. Si un usuario había aplicado colores personalizados, ese atributo se pierde en la primera re-serialización. El resto del formato es lossless.
Migración soft (backward-compat)
No se requiere migración SQL. La detección del formato legacy es por heurística:
// NoteEditor.tsx — initialContent
const raw = note.content?.trim() ?? '';
if (!raw || raw === '{}') return '';
if (raw.startsWith('{')) {
try { return JSON.parse(raw); } catch { /* fallthrough */ }
}
return raw; // ya es Markdown
- Si
contentempieza por{→ se parsea como JSON y se pasa al editor como objeto TipTap. - En el siguiente auto-save, el editor serializa en Markdown y sobreescribe el campo
contentcon el nuevo formato. - La migración es transparente y sucede la primera vez que el usuario edita cada nota legacy.
Auto-save (NoteEditor.tsx)
onUpdate: ({ editor }) => {
const md = (editor.storage as { markdown?: { getMarkdown: () => string } }).markdown;
const content = md ? md.getMarkdown() : '';
await onPersist({ content, word_count: wordCount });
}
El word_count sigue calculándose con editor.getText() (texto sin formato).
Exportación — 3 formatos
El componente ExportModal.tsx gestiona los tres formatos mediante un editor fantasma (ghost editor, editable: false) que parsea el storage sin renderizarlo en pantalla.
| Formato | Extensión | MIME | Método |
|---|---|---|---|
| Texto plano | .txt | text/plain;charset=utf-8 | editor.getText() |
| Markdown | .md | text/markdown;charset=utf-8 | Storage canónico directo |
| HTML standalone | .html | text/html;charset=utf-8 | marked.parse(markdown) + wrapper |
Flujo interno del ghost editor
// Detecta formato del storage para inicializar el ghost editor
const initialContent = useMemo(() => {
const raw = note.content?.trim() ?? '';
if (!raw || raw === '{}') return '';
if (raw.startsWith('{')) {
try { return JSON.parse(raw); } catch { /* fallthrough */ }
}
return raw; // Markdown
}, [note.content]);
El Markdown canónico se obtiene:
- Si
initialContentesstring→ ya es Markdown, se usa directamente. - Si es JSON object → se pasa al ghost editor y se extrae con
ghostEditor.storage.markdown.getMarkdown().
Exportación HTML con marked (GFM)
La biblioteca marked@18.0.3 convierte el Markdown canónico a HTML con soporte GFM completo:
marked.use({ gfm: true, breaks: false });
// ...
return marked.parse(markdown, { async: false }) as string;
El HTML generado se envuelve en un documento standalone mediante wrapHtml():
<!DOCTYPE html>+<meta charset="utf-8">- CSS embebido: tipografía, tablas con cabecera, listas de tareas (
.contains-task-list), bloques de código, citas, separadores. <title>= título de la nota.
PDF: La exportación a PDF no se gestiona programáticamente. Se recomienda Ctrl+P → “Guardar como PDF” desde el archivo HTML descargado o desde el navegador.
Soporte GFM en el HTML exportado
| Elemento GFM | Soporte |
|---|---|
| Tablas | ✅ border-collapse, cabecera gris |
| Listas de tareas | ✅ clases contains-task-list (marked estándar) |
| Strikethrough | ✅ nativo GFM |
| Bloques de código | ✅ <pre><code> con fondo gris |
| Citas | ✅ border-left + color muted |
Separadores --- | ✅ <hr> con estilo |
Dependencia añadida
"marked": "^18.0.3"
- Motor de render Markdown → HTML. Requiere Node ≥ 20.
- Modo síncrono (
async: false) para compatibilidad conuseMemosinuseEffect. - Se usa solo en el cliente (componente React), nunca en SSR.
Archivos afectados
| Archivo | Cambio |
|---|---|
src/components/notebook/NoteEditor.tsx | Storage canónico → Markdown en auto-save |
src/components/notebook/ExportModal.tsx | Nuevo formato TXT, conversión MD→HTML con marked, backward-compat |
package.json | Nueva dep marked@18.0.3 |
pnpm-lock.yaml | Lock de marked y sus snaps |
public/search-index.json | Re-indexación automática |
Consideraciones operativas
- Sin migración SQL: el cambio es transparente. Las notas legacy se migran al primer edit.
- Riesgo de pérdida de color: mínimo impacto (feature poco usada), documentado y aceptado.
- Reversión: si se necesita revertir, las notas ya migradas tendrán Markdown en
content; el editor puede cargarlo igualmente (tiptap-markdown lo parsea). No hay pérdida de datos funcionales. - Búsqueda full-text:
public/search-index.jsonse regenera. El texto indexado ahora proviene del Markdown serializado, no del JSON.
Véase también
- [[workspace—tareas-notas]]
- [[workspace—que-es-workspace]]