fix: 5 bugs critiques de l'éditeur (Phase 1 audit)
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 5m39s
CI / Deploy production (on server) (push) Successful in 22s

1. replaceAll (Find & Replace) — une seule transaction ProseMirror
   au lieu d'un forEach cassé. Tous les matchs sont maintenant remplacés.

2. Link Preview unwrap — deleteNode() au lieu de clearer les attrs
   qui laissaient un nœud fantôme invisible dans le document.

3. Conversion Markdown → richtext — breaks: true dans marked.parse()
   Les simple newlines sont maintenant convertis en <br>.
   + préserve les blocs custom (toggle, callout, math, columns,
   outline, link-preview) en commentaires HTML lors de l'export MD.

4. emitNoteChange exercices — shape corrigée (type:'created' attend
   un objet Note, pas noteId/notebookId séparés).

5. Raccourcis clavier sans conflit :
   Cmd+Shift+C → Cmd+Alt+C (callout, avant: copier)
   Cmd+Shift+O → Cmd+Alt+O (outline, avant: historique/signets)
   Cmd+Shift+L → Cmd+Alt+L (colonnes, avant: lock screen macOS)
This commit is contained in:
Antigravity
2026-06-20 15:48:18 +00:00
parent 5b13a88b72
commit ee70e74bf5
51 changed files with 1483 additions and 252 deletions

View File

@@ -121,7 +121,7 @@ interface BlockPlaceholder {
function preprocessCustomNodes(html: string): { html: string; placeholders: BlockPlaceholder[] } {
const placeholders: BlockPlaceholder[] = []
// liveBlock: <div data-live-block="true" sourceNoteId="..." blockId="..."></div>
// liveBlock
let result = html.replace(
/<div([^>]*?data-live-block[^>]*?)>\s*<\/div>/gi,
(_match, attrs) => {
@@ -133,7 +133,7 @@ function preprocessCustomNodes(html: string): { html: string; placeholders: Bloc
}
)
// structuredViewBlock: <div data-structured-view-block="true" ...></div>
// structuredViewBlock
result = result.replace(
/<div([^>]*?data-structured-view-block[^>]*?)>\s*<\/div>/gi,
(_match, attrs) => {
@@ -149,6 +149,71 @@ function preprocessCustomNodes(html: string): { html: string; placeholders: Bloc
}
)
// toggleBlock — preserve as HTML comment
result = result.replace(
/<div([^>]*?data-type="toggle-block"[^>]*?)>([\s\S]*?)<\/div>/gi,
(_match, attrs, content) => {
const opened = attrs.match(/data-opened="([^"]*)"/i)?.[1] || 'true'
const key = `${SENTINEL_PREFIX}TOGGLE${placeholders.length}`
placeholders.push({ key, comment: `<!-- toggle: opened=${opened} -->${content}\n<!-- /toggle -->` })
return `<p>${key}</p>`
}
)
// calloutBlock
result = result.replace(
/<div([^>]*?data-type="callout-block"[^>]*?)>([\s\S]*?)<\/div>/gi,
(_match, attrs, content) => {
const type = attrs.match(/data-callout-type="([^"]*)"/i)?.[1] || 'info'
const key = `${SENTINEL_PREFIX}CALLOUT${placeholders.length}`
placeholders.push({ key, comment: `<!-- callout: type=${type} -->${content}\n<!-- /callout -->` })
return `<p>${key}</p>`
}
)
// mathEquationBlock
result = result.replace(
/<div([^>]*?data-type="math-equation"[^>]*?)>([\s\S]*?)<\/div>/gi,
(_match, attrs, content) => {
const latex = attrs.match(/data-latex="([^"]*)"/i)?.[1] || content.trim()
const key = `${SENTINEL_PREFIX}MATH${placeholders.length}`
placeholders.push({ key, comment: `<!-- math: ${latex} -->` })
return `<p>${key}</p>`
}
)
// outlineBlock
result = result.replace(
/<div[^>]*data-type="outline-block"[^>]*><\/div>/gi,
() => {
const key = `${SENTINEL_PREFIX}OUTLINE${placeholders.length}`
placeholders.push({ key, comment: `<!-- outline -->` })
return `<p>${key}</p>`
}
)
// columns
result = result.replace(
/<div([^>]*?data-type="columns"[^>]*?)>([\s\S]*?)<\/div>/gi,
(_match, attrs, content) => {
const cols = attrs.match(/cols="([^"]*)"/i)?.[1] || '2'
const key = `${SENTINEL_PREFIX}COLUMNS${placeholders.length}`
placeholders.push({ key, comment: `<!-- columns: cols=${cols} -->${content}\n<!-- /columns -->` })
return `<p>${key}</p>`
}
)
// linkPreviewBlock
result = result.replace(
/<div([^>]*?data-type="link-preview-block"[^>]*?)>[\s\S]*?<\/div>/gi,
(_match, attrs) => {
const url = attrs.match(/data-url="([^"]*)"/i)?.[1] || ''
const key = `${SENTINEL_PREFIX}LINKPREVIEW${placeholders.length}`
placeholders.push({ key, comment: `<!-- link-preview: ${url} -->` })
return `<p>${key}</p>`
}
)
return { html: result, placeholders }
}
@@ -188,10 +253,10 @@ export function tiptapHTMLToMarkdown(html: string): string {
export function markdownToHTML(markdown: string): string {
if (!markdown || markdown.trim() === '') return ''
// marked v18+ uses synchronous parse by default when no async tokens
// breaks: true — single \n becomes <br>, matching WYSIWYG expectations
const html = marked.parse(markdown, {
gfm: true,
breaks: false,
breaks: true,
}) as string
return html