Files
blackheart-website/src/lib/markup.tsx
T

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}</>;
}