84 lines
2.3 KiB
TypeScript
84 lines
2.3 KiB
TypeScript
import React from "react";
|
|
|
|
/**
|
|
* Parse inline markup: **bold** and *italic*.
|
|
* Returns React elements safe from XSS.
|
|
*/
|
|
function parseInline(text: string, keyPrefix: string): React.ReactNode[] {
|
|
const parts: React.ReactNode[] = [];
|
|
const regex = /(\*\*(.+?)\*\*|\*(.+?)\*)/g;
|
|
let lastIndex = 0;
|
|
let match: RegExpExecArray | null;
|
|
let key = 0;
|
|
|
|
while ((match = regex.exec(text)) !== null) {
|
|
if (match.index > lastIndex) {
|
|
parts.push(text.slice(lastIndex, match.index));
|
|
}
|
|
if (match[2]) {
|
|
parts.push(<strong key={`${keyPrefix}-b${key++}`} className="font-semibold text-neutral-900 dark:text-white">{match[2]}</strong>);
|
|
} else if (match[3]) {
|
|
parts.push(<em key={`${keyPrefix}-i${key++}`}>{match[3]}</em>);
|
|
}
|
|
lastIndex = match.index + match[0].length;
|
|
}
|
|
|
|
if (lastIndex < text.length) {
|
|
parts.push(text.slice(lastIndex));
|
|
}
|
|
|
|
return parts;
|
|
}
|
|
|
|
/**
|
|
* Simple markup renderer.
|
|
* Supports: **bold**, *italic*, ## headings, — bullet points, blank lines for paragraphs.
|
|
*/
|
|
export function formatMarkup(text: string): React.ReactNode {
|
|
if (!text) return null;
|
|
|
|
const lines = text.split("\n");
|
|
const elements: React.ReactNode[] = [];
|
|
let key = 0;
|
|
|
|
for (const line of lines) {
|
|
const trimmed = line.trimStart();
|
|
|
|
// ## Heading
|
|
if (trimmed.startsWith("## ")) {
|
|
elements.push(
|
|
<span key={key++} className="block mt-3 mb-1 text-sm font-semibold text-neutral-900 dark:text-white">
|
|
{parseInline(trimmed.slice(3), `h${key}`)}
|
|
</span>
|
|
);
|
|
continue;
|
|
}
|
|
|
|
// — Bullet point
|
|
if (trimmed.startsWith("— ") || trimmed.startsWith("- ") || trimmed.startsWith("🤍 ")) {
|
|
const content = trimmed.startsWith("🤍 ") ? trimmed.slice(3) : trimmed.slice(2);
|
|
elements.push(
|
|
<span key={key++} className="block pl-6 before:content-['🤍'] before:absolute before:left-0 relative">
|
|
{parseInline(content, `li${key}`)}
|
|
</span>
|
|
);
|
|
continue;
|
|
}
|
|
|
|
// Empty line = paragraph break
|
|
if (trimmed === "") {
|
|
elements.push(<span key={key++} className="block h-2" />);
|
|
continue;
|
|
}
|
|
|
|
// Regular line with inline markup
|
|
elements.push(
|
|
<span key={key++} className="block">
|
|
{parseInline(line, `p${key}`)}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
return <>{elements}</>;
|
|
}
|