134 lines
3.7 KiB
JavaScript
134 lines
3.7 KiB
JavaScript
// Lightweight Markdown renderer – handles the subset used by the AI:
|
||
// ## Headings, **bold**, bullet lists, numbered lists, line breaks
|
||
|
||
export default function Markdown({ text }) {
|
||
if (!text) return null
|
||
|
||
const lines = text.split('\n')
|
||
const elements = []
|
||
let i = 0
|
||
|
||
const parseLine = (line) => {
|
||
// Parse inline **bold** and *italic*
|
||
const parts = []
|
||
let remaining = line
|
||
let key = 0
|
||
while (remaining.length > 0) {
|
||
const boldMatch = remaining.match(/^(.*?)\*\*(.*?)\*\*(.*)$/)
|
||
if (boldMatch) {
|
||
if (boldMatch[1]) parts.push(<span key={key++}>{boldMatch[1]}</span>)
|
||
parts.push(<strong key={key++}>{boldMatch[2]}</strong>)
|
||
remaining = boldMatch[3]
|
||
continue
|
||
}
|
||
const italicMatch = remaining.match(/^(.*?)\*(.*?)\*(.*)$/)
|
||
if (italicMatch) {
|
||
if (italicMatch[1]) parts.push(<span key={key++}>{italicMatch[1]}</span>)
|
||
parts.push(<em key={key++}>{italicMatch[2]}</em>)
|
||
remaining = italicMatch[3]
|
||
continue
|
||
}
|
||
parts.push(<span key={key++}>{remaining}</span>)
|
||
break
|
||
}
|
||
return parts.length === 1 && typeof parts[0].props?.children === 'string'
|
||
? parts[0].props.children
|
||
: parts
|
||
}
|
||
|
||
while (i < lines.length) {
|
||
const line = lines[i]
|
||
|
||
// Skip empty lines (add spacing)
|
||
if (line.trim() === '') {
|
||
elements.push(<div key={i} style={{ height: 8 }} />)
|
||
i++; continue
|
||
}
|
||
|
||
// H1
|
||
if (line.startsWith('# ')) {
|
||
elements.push(
|
||
<h1 key={i} style={{ fontSize: 18, fontWeight: 700, margin: '16px 0 8px', color: 'var(--text1)' }}>
|
||
{parseLine(line.slice(2))}
|
||
</h1>
|
||
)
|
||
i++; continue
|
||
}
|
||
|
||
// H2
|
||
if (line.startsWith('## ')) {
|
||
elements.push(
|
||
<h2 key={i} style={{ fontSize: 15, fontWeight: 700, margin: '14px 0 6px', color: 'var(--text1)',
|
||
display: 'flex', alignItems: 'center', gap: 6 }}>
|
||
{parseLine(line.slice(3))}
|
||
</h2>
|
||
)
|
||
i++; continue
|
||
}
|
||
|
||
// H3
|
||
if (line.startsWith('### ')) {
|
||
elements.push(
|
||
<h3 key={i} style={{ fontSize: 14, fontWeight: 600, margin: '10px 0 4px', color: 'var(--text1)' }}>
|
||
{parseLine(line.slice(4))}
|
||
</h3>
|
||
)
|
||
i++; continue
|
||
}
|
||
|
||
// Unordered list item
|
||
if (line.match(/^[-*] /)) {
|
||
const listItems = []
|
||
while (i < lines.length && lines[i].match(/^[-*] /)) {
|
||
listItems.push(
|
||
<li key={i} style={{ fontSize: 13, lineHeight: 1.65, color: 'var(--text2)', marginBottom: 4 }}>
|
||
{parseLine(lines[i].slice(2))}
|
||
</li>
|
||
)
|
||
i++
|
||
}
|
||
elements.push(
|
||
<ul key={`ul-${i}`} style={{ paddingLeft: 20, margin: '6px 0' }}>
|
||
{listItems}
|
||
</ul>
|
||
)
|
||
continue
|
||
}
|
||
|
||
// Numbered list item
|
||
if (line.match(/^\d+\. /)) {
|
||
const listItems = []
|
||
while (i < lines.length && lines[i].match(/^\d+\. /)) {
|
||
listItems.push(
|
||
<li key={i} style={{ fontSize: 13, lineHeight: 1.65, color: 'var(--text2)', marginBottom: 4 }}>
|
||
{parseLine(lines[i].replace(/^\d+\. /, ''))}
|
||
</li>
|
||
)
|
||
i++
|
||
}
|
||
elements.push(
|
||
<ol key={`ol-${i}`} style={{ paddingLeft: 20, margin: '6px 0' }}>
|
||
{listItems}
|
||
</ol>
|
||
)
|
||
continue
|
||
}
|
||
|
||
// Horizontal rule
|
||
if (line.match(/^---+$/)) {
|
||
elements.push(<hr key={i} style={{ border: 'none', borderTop: '1px solid var(--border)', margin: '12px 0' }} />)
|
||
i++; continue
|
||
}
|
||
|
||
// Normal paragraph
|
||
elements.push(
|
||
<p key={i} style={{ fontSize: 13, lineHeight: 1.7, color: 'var(--text2)', margin: '4px 0' }}>
|
||
{parseLine(line)}
|
||
</p>
|
||
)
|
||
i++
|
||
}
|
||
|
||
return <div>{elements}</div>
|
||
}
|