From f81be12a38bc3002d59c7275476a2556ef47b6f1 Mon Sep 17 00:00:00 2001 From: Gilles Soulier Date: Mon, 25 May 2026 20:13:14 +0200 Subject: [PATCH] =?UTF-8?q?feat(notes):=20renderer=20markdown=20=C3=A9tend?= =?UTF-8?q?u=20v0.5.9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tableaux (pipe syntax) avec header distinct - Task lists - [ ] / - [x] avec checkbox colorée - Listes imbriquées avec indentation (• / ◦) - Texte barré ~~strikethrough~~ - Liens [texte](url) cliquables (target _blank) Co-Authored-By: Claude Sonnet 4.6 --- frontend/package.json | 2 +- frontend/src/pages/NotesPage.tsx | 83 +++++++++++++++++++++++++++++--- 2 files changed, 78 insertions(+), 7 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index ab1e65c..635632b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "homehub-frontend", "private": true, - "version": "0.5.8", + "version": "0.5.9", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/pages/NotesPage.tsx b/frontend/src/pages/NotesPage.tsx index d4b1248..477eab0 100644 --- a/frontend/src/pages/NotesPage.tsx +++ b/frontend/src/pages/NotesPage.tsx @@ -36,15 +36,18 @@ function formatDate(iso: string) { return new Date(iso).toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', year: 'numeric' }) } -// Formate le texte inline : **gras**, *italique*, `code` +// Formate le texte inline : **gras**, *italique*, ~~barré~~, `code`, [lien](url) function inlineFmt(text: string): React.ReactNode { - const parts = text.split(/(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/) + const parts = text.split(/(\*\*[^*]+\*\*|\*[^*]+\*|~~[^~]+~~|`[^`]+`|\[[^\]]+\]\([^)]+\))/) return ( <> {parts.map((p, i) => { if (p.startsWith('**') && p.endsWith('**')) return {p.slice(2, -2)} + if (p.startsWith('~~') && p.endsWith('~~')) return {p.slice(2, -2)} if (p.startsWith('*') && p.endsWith('*')) return {p.slice(1, -1)} if (p.startsWith('`') && p.endsWith('`')) return {p.slice(1, -1)} + const linkMatch = p.match(/^\[([^\]]+)\]\(([^)]+)\)$/) + if (linkMatch) return {linkMatch[1]} return p || null })} @@ -58,6 +61,8 @@ function renderMarkdown(text: string): React.ReactNode { let i = 0 while (i < lines.length) { const line = lines[i] + + // Blocs de code if (line.startsWith('```')) { const lang = line.slice(3).trim() const code: string[] = [] @@ -69,19 +74,77 @@ function renderMarkdown(text: string): React.ReactNode { {code.join('\n')} ) + + // Tableaux : ligne commençant et finissant par | + } else if (line.startsWith('|') && line.trim().endsWith('|')) { + const tableLines: string[] = [line] + i++ + while (i < lines.length && lines[i].startsWith('|') && lines[i].trim().endsWith('|')) { + tableLines.push(lines[i]) + i++ + } + const parseRow = (r: string) => r.split('|').slice(1, -1).map(c => c.trim()) + const isSep = (r: string) => /^\|[\s|:-]+\|$/.test(r.trim()) + const headers = parseRow(tableLines[0]) + const bodyRows = tableLines.filter((_, idx) => idx > 0 && !isSep(tableLines[idx])) + nodes.push( +
+ + + + {headers.map((h, hi) => ( + + ))} + + + + {bodyRows.map((row, ri) => ( + + {parseRow(row).map((cell, ci) => ( + + ))} + + ))} + +
{inlineFmt(h)}
{inlineFmt(cell)}
+
+ ) + continue + + // Titres } else if (line.startsWith('# ')) { nodes.push(
{inlineFmt(line.slice(2))}
) } else if (line.startsWith('## ')) { nodes.push(
{inlineFmt(line.slice(3))}
) } else if (line.startsWith('### ')) { nodes.push(
{inlineFmt(line.slice(4))}
) - } else if (line.startsWith('- ') || line.startsWith('* ')) { + + // Task lists : - [ ] et - [x] + } else if (/^(\s*)-\s\[([ xX])\]\s/.test(line)) { + const m = line.match(/^(\s*)-\s\[([ xX])\]\s(.*)/) + const indent = (m?.[1] ?? '').length + const done = (m?.[2] ?? ' ').toLowerCase() === 'x' nodes.push( -
- - {inlineFmt(line.slice(2))} +
+ + {done && } + + {inlineFmt(m?.[3] ?? '')}
) + + // Listes non ordonnées (avec indentation possible) + } else if (/^(\s*)[-*]\s/.test(line)) { + const m = line.match(/^(\s*)[-*]\s(.*)/) + const indent = (m?.[1] ?? '').length + nodes.push( +
+ {indent > 0 ? '◦' : '•'} + {inlineFmt(m?.[2] ?? '')} +
+ ) + + // Listes ordonnées } else if (/^\d+\.\s/.test(line)) { const m = line.match(/^(\d+)\.\s(.*)/) nodes.push( @@ -90,16 +153,24 @@ function renderMarkdown(text: string): React.ReactNode { {inlineFmt(m?.[2] ?? '')}
) + + // Citation } else if (line.startsWith('> ')) { nodes.push(
{inlineFmt(line.slice(2))}
) + + // Séparateur } else if (line === '---' || line === '***') { nodes.push(
) + + // Ligne vide } else if (line.trim() === '') { nodes.push(
) + + // Paragraphe normal } else { nodes.push(