blob: 4f09644534bd00631e14461f9781e6fbf3fbd54a (
plain) (
tree)
|
|
import { useMemo } from "react";
interface SimpleMarkdownProps {
content: string;
className?: string;
}
/**
* A simplistic markdown renderer that handles:
* - Newlines (paragraphs)
* - Bold (**text**)
* - Inline code (`code`)
* - Code blocks (```code```)
* - Headers (# ## ###)
* - Lists (- item)
*/
export function SimpleMarkdown({ content, className = "" }: SimpleMarkdownProps) {
const rendered = useMemo(() => {
if (!content) return null;
// Split by code blocks first to handle them separately
const parts = content.split(/(```[\s\S]*?```)/g);
return parts.map((part, partIndex) => {
// Handle code blocks
if (part.startsWith("```") && part.endsWith("```")) {
const code = part.slice(3, -3).replace(/^\w+\n/, ""); // Remove language hint
return (
<pre
key={partIndex}
className="bg-[#0a1525] border border-[rgba(117,170,252,0.2)] p-2 my-1 overflow-x-auto text-[#9bc3ff] text-[10px]"
>
<code>{code.trim()}</code>
</pre>
);
}
// Split by newlines and process each line
const lines = part.split("\n");
return lines.map((line, lineIndex) => {
const key = `${partIndex}-${lineIndex}`;
// Skip empty lines but add spacing
if (!line.trim()) {
return <div key={key} className="h-1" />;
}
// Headers
if (line.startsWith("### ")) {
return (
<div key={key} className="font-bold text-white/90 mt-2 mb-1">
{processInline(line.slice(4))}
</div>
);
}
if (line.startsWith("## ")) {
return (
<div key={key} className="font-bold text-white mt-2 mb-1">
{processInline(line.slice(3))}
</div>
);
}
if (line.startsWith("# ")) {
return (
<div key={key} className="font-bold text-white text-sm mt-2 mb-1">
{processInline(line.slice(2))}
</div>
);
}
// List items
if (line.match(/^[-*]\s/)) {
return (
<div key={key} className="flex gap-1">
<span className="text-[#555]">-</span>
<span>{processInline(line.slice(2))}</span>
</div>
);
}
// Numbered list items
if (line.match(/^\d+\.\s/)) {
const match = line.match(/^(\d+)\.\s(.*)$/);
if (match) {
return (
<div key={key} className="flex gap-1">
<span className="text-[#555]">{match[1]}.</span>
<span>{processInline(match[2])}</span>
</div>
);
}
}
// Regular paragraph
return <div key={key}>{processInline(line)}</div>;
});
});
}, [content]);
return <div className={`space-y-0.5 ${className}`}>{rendered}</div>;
}
/**
* Process inline markdown: bold, inline code
*/
function processInline(text: string): React.ReactNode {
if (!text) return null;
// Split by inline code and bold patterns
const parts: React.ReactNode[] = [];
let remaining = text;
let keyIndex = 0;
while (remaining.length > 0) {
// Check for inline code first
const codeMatch = remaining.match(/^(.*?)`([^`]+)`(.*)$/);
if (codeMatch) {
if (codeMatch[1]) {
parts.push(...processInlineBold(codeMatch[1], keyIndex++));
}
parts.push(
<code
key={`code-${keyIndex++}`}
className="bg-[#0a1525] px-1 py-0.5 text-[#9bc3ff] text-[10px]"
>
{codeMatch[2]}
</code>
);
remaining = codeMatch[3];
continue;
}
// No more inline code, process bold in remaining text
parts.push(...processInlineBold(remaining, keyIndex));
break;
}
return parts.length === 1 ? parts[0] : parts;
}
/**
* Process bold text (**text**)
*/
function processInlineBold(text: string, startKey: number): React.ReactNode[] {
const parts: React.ReactNode[] = [];
let remaining = text;
let keyIndex = startKey;
while (remaining.length > 0) {
const boldMatch = remaining.match(/^(.*?)\*\*([^*]+)\*\*(.*)$/);
if (boldMatch) {
if (boldMatch[1]) {
parts.push(<span key={`text-${keyIndex++}`}>{boldMatch[1]}</span>);
}
parts.push(
<strong key={`bold-${keyIndex++}`} className="text-white/90">
{boldMatch[2]}
</strong>
);
remaining = boldMatch[3];
continue;
}
// No more bold patterns
if (remaining) {
parts.push(<span key={`text-${keyIndex++}`}>{remaining}</span>);
}
break;
}
return parts;
}
|