feat(notes): renderer markdown étendu v0.5.9
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "homehub-frontend",
|
||||
"private": true,
|
||||
"version": "0.5.8",
|
||||
"version": "0.5.9",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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 <strong key={i}>{p.slice(2, -2)}</strong>
|
||||
if (p.startsWith('~~') && p.endsWith('~~')) return <del key={i} style={{ color: 'var(--ink-4)' }}>{p.slice(2, -2)}</del>
|
||||
if (p.startsWith('*') && p.endsWith('*')) return <em key={i} style={{ color: 'var(--ink-3)' }}>{p.slice(1, -1)}</em>
|
||||
if (p.startsWith('`') && p.endsWith('`')) return <code key={i} style={{ background: 'var(--bg-4)', borderRadius: 3, padding: '0 4px', fontFamily: 'var(--font-mono)', fontSize: '0.88em' }}>{p.slice(1, -1)}</code>
|
||||
const linkMatch = p.match(/^\[([^\]]+)\]\(([^)]+)\)$/)
|
||||
if (linkMatch) return <a key={i} href={linkMatch[2]} target="_blank" rel="noopener noreferrer" style={{ color: 'var(--accent)', textDecoration: 'underline' }}>{linkMatch[1]}</a>
|
||||
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')}
|
||||
</pre>
|
||||
)
|
||||
|
||||
// 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(
|
||||
<div key={i} style={{ overflowX: 'auto', margin: '6px 0' }}>
|
||||
<table style={{ borderCollapse: 'collapse', width: '100%', fontSize: 12, fontFamily: 'var(--font-ui)' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
{headers.map((h, hi) => (
|
||||
<th key={hi} style={{ border: '1px solid var(--bg-5)', padding: '4px 8px', background: 'var(--bg-4)', color: 'var(--ink-2)', fontWeight: 600, textAlign: 'left' }}>{inlineFmt(h)}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{bodyRows.map((row, ri) => (
|
||||
<tr key={ri}>
|
||||
{parseRow(row).map((cell, ci) => (
|
||||
<td key={ci} style={{ border: '1px solid var(--bg-5)', padding: '4px 8px', color: 'var(--ink-2)' }}>{inlineFmt(cell)}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
continue
|
||||
|
||||
// Titres
|
||||
} else if (line.startsWith('# ')) {
|
||||
nodes.push(<div key={i} style={{ fontWeight: 700, fontSize: 15, color: 'var(--accent)', marginTop: 10, marginBottom: 2, fontFamily: 'var(--font-ui)' }}>{inlineFmt(line.slice(2))}</div>)
|
||||
} else if (line.startsWith('## ')) {
|
||||
nodes.push(<div key={i} style={{ fontWeight: 700, fontSize: 14, color: 'var(--ink-1)', marginTop: 8, marginBottom: 2, paddingBottom: 3, borderBottom: '1px solid var(--bg-5)', fontFamily: 'var(--font-ui)' }}>{inlineFmt(line.slice(3))}</div>)
|
||||
} else if (line.startsWith('### ')) {
|
||||
nodes.push(<div key={i} style={{ fontWeight: 600, fontSize: 13, color: 'var(--ink-1)', marginTop: 6, marginBottom: 1, fontFamily: 'var(--font-ui)' }}>{inlineFmt(line.slice(4))}</div>)
|
||||
} 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(
|
||||
<div key={i} style={{ display: 'flex', gap: 6, color: 'var(--ink-2)', lineHeight: 1.6, fontFamily: 'var(--font-ui)', fontSize: 13 }}>
|
||||
<span style={{ color: 'var(--accent)', flexShrink: 0 }}>•</span>
|
||||
<span>{inlineFmt(line.slice(2))}</span>
|
||||
<div key={i} style={{ display: 'flex', gap: 6, alignItems: 'flex-start', marginLeft: indent * 8, color: done ? 'var(--ink-4)' : 'var(--ink-2)', lineHeight: 1.6, fontFamily: 'var(--font-ui)', fontSize: 13 }}>
|
||||
<span style={{ flexShrink: 0, width: 14, height: 14, marginTop: 3, border: `1.5px solid ${done ? 'var(--ok)' : 'var(--ink-4)'}`, borderRadius: 3, display: 'flex', alignItems: 'center', justifyContent: 'center', background: done ? 'var(--ok)' : 'transparent' }}>
|
||||
{done && <i className="fa-solid fa-check" style={{ fontSize: 8, color: 'var(--bg-1)' }} />}
|
||||
</span>
|
||||
<span style={{ textDecoration: done ? 'line-through' : 'none' }}>{inlineFmt(m?.[3] ?? '')}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
// 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(
|
||||
<div key={i} style={{ display: 'flex', gap: 6, marginLeft: indent * 8, color: 'var(--ink-2)', lineHeight: 1.6, fontFamily: 'var(--font-ui)', fontSize: 13 }}>
|
||||
<span style={{ color: 'var(--accent)', flexShrink: 0 }}>{indent > 0 ? '◦' : '•'}</span>
|
||||
<span>{inlineFmt(m?.[2] ?? '')}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
// 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 {
|
||||
<span>{inlineFmt(m?.[2] ?? '')}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
// Citation
|
||||
} else if (line.startsWith('> ')) {
|
||||
nodes.push(
|
||||
<div key={i} style={{ borderLeft: '3px solid var(--accent)', paddingLeft: 10, color: 'var(--ink-3)', fontStyle: 'italic', margin: '4px 0', lineHeight: 1.6, fontFamily: 'var(--font-ui)', fontSize: 13 }}>
|
||||
{inlineFmt(line.slice(2))}
|
||||
</div>
|
||||
)
|
||||
|
||||
// Séparateur
|
||||
} else if (line === '---' || line === '***') {
|
||||
nodes.push(<div key={i} style={{ borderBottom: '1px solid var(--bg-5)', margin: '8px 0' }} />)
|
||||
|
||||
// Ligne vide
|
||||
} else if (line.trim() === '') {
|
||||
nodes.push(<div key={i} style={{ height: 5 }} />)
|
||||
|
||||
// Paragraphe normal
|
||||
} else {
|
||||
nodes.push(
|
||||
<div key={i} style={{ color: 'var(--ink-2)', lineHeight: 1.6, fontFamily: 'var(--font-ui)', fontSize: 13 }}>
|
||||
|
||||
Reference in New Issue
Block a user