fix: 5 bugs critiques de l'éditeur (Phase 1 audit)
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user